@devrik-tools/claude-gates 0.7.2 → 0.9.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 +3 -3
- package/README.es.md +129 -11
- package/README.md +120 -10
- package/cli/doctor.mjs +132 -0
- package/cli/evidence.mjs +78 -0
- package/cli/hooks-manifest.mjs +89 -0
- package/cli/index.mjs +124 -6
- package/cli/init.mjs +97 -3
- package/cli/log.mjs +70 -0
- package/cli/materialize.mjs +36 -2
- package/cli/registry.mjs +12 -1
- package/cli/selection.mjs +18 -0
- package/cli/smoke-fixtures.json +159 -25
- package/cli/task.mjs +104 -11
- package/cli/toggle.mjs +125 -0
- package/package.json +4 -3
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/doctor.mjs +47 -1
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
- package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +94 -215
- package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
- package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
- package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
- package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
- package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
- package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +53 -95
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
- package/plugins/gates/hooks/gates/library-docs/index.mjs +338 -0
- package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
- package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
- package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
- package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
- package/plugins/gates/hooks/gates/no-trivial-scripts/index.mjs +114 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
- package/plugins/gates/hooks/gates/require-monitor/index.mjs +126 -0
- package/plugins/gates/hooks/gates/require-task-split/index.mjs +88 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
- package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
- package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
- package/plugins/gates/hooks/hooks.json +152 -42
- package/plugins/gates/hooks/lib/config.mjs +6 -3
- package/plugins/gates/hooks/lib/delegation.mjs +352 -0
- package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
- package/plugins/gates/hooks/lib/git.mjs +262 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +451 -83
- package/plugins/gates/hooks/lib/session-state.mjs +150 -0
- package/plugins/gates/hooks/lib/signals.mjs +18 -13
- package/plugins/gates/hooks/lib/testing.mjs +148 -0
- package/plugins/gates/hooks/lib/tools.mjs +144 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/lib/task-store.mjs +12 -6
- package/plugins/tasks/hooks/register-requests.mjs +37 -10
- package/registry.json +249 -38
|
@@ -1,198 +1,45 @@
|
|
|
1
|
-
// risk-level — denies an implementation delegation that does not declare its risk
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// risk-level — denies an implementation delegation that does not declare its risk LEVEL
|
|
2
|
+
// (QUESTION|MICRO|STANDARD|HIGH-RISK), and denies a declared level that contradicts a real
|
|
3
|
+
// high-impact signal in the prompt (anything but HIGH-RISK).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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.
|
|
5
|
+
// Decisions: classifying between QUESTION/MICRO/STANDARD stays the delegator's judgment;
|
|
6
|
+
// the gate only imposes that the declaration EXISTS and does not contradict a real,
|
|
7
|
+
// non-quoted high-impact signal. Over-declaring HIGH-RISK is never penalized. The LAST
|
|
8
|
+
// declaration governs, and contradictory declarations deny rather than guess. A documentary
|
|
9
|
+
// deliverable (a report on the payment flow) is not an implementation and needs no LEVEL.
|
|
10
|
+
// An empty `highImpactPatterns` turns the contradiction check off, not the LEVEL requirement.
|
|
32
11
|
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_HIGH_IMPACT_PATTERNS,
|
|
14
|
+
DEFAULT_READ_ONLY_SUBAGENTS,
|
|
15
|
+
LEVELS,
|
|
16
|
+
allMatches,
|
|
17
|
+
buildHighImpactPattern,
|
|
18
|
+
declaredLevelsOf,
|
|
19
|
+
hasAmbiguousLevel,
|
|
20
|
+
hasRealSensitiveMutation,
|
|
21
|
+
isDocumentaryRequest,
|
|
22
|
+
isExemptQuery,
|
|
23
|
+
isImplementationRequest,
|
|
24
|
+
isReadOnlySubagent,
|
|
25
|
+
operativeLevelOf,
|
|
26
|
+
promptExcerpt,
|
|
27
|
+
stripQuoted,
|
|
28
|
+
} from '../../lib/delegation.mjs';
|
|
33
29
|
import {
|
|
34
30
|
runGate,
|
|
35
31
|
deny,
|
|
36
32
|
toolInGroups,
|
|
37
33
|
delegationPromptOf,
|
|
38
34
|
} from '../../lib/hook-io.mjs';
|
|
39
|
-
import { MUTATION_RISK_SIGNAL } from '../../lib/signals.mjs';
|
|
40
35
|
|
|
41
36
|
const GATE_ID = 'risk-level';
|
|
42
37
|
const CONFIG_KEY = 'requireDeclaredRiskLevel';
|
|
43
38
|
|
|
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) =>
|
|
173
|
-
match[2].toUpperCase(),
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** The operative LEVEL: the LAST declaration in the prompt (a delegator who corrects an
|
|
178
|
-
* earlier decoy/reference mention writes the real value last). Returns `{ level }` when
|
|
179
|
-
* every declaration agrees or there is exactly one; returns `{ ambiguous: true }` when
|
|
180
|
-
* two or more DIFFERENT levels are declared — the gate cannot know which one governs,
|
|
181
|
-
* so it asks for a single unambiguous LEVEL rather than silently picking one. */
|
|
182
|
-
function operativeLevel(prompt) {
|
|
183
|
-
const levels = declaredLevels(prompt);
|
|
184
|
-
if (levels.length === 0) return { level: null };
|
|
185
|
-
const distinct = new Set(levels);
|
|
186
|
-
if (distinct.size > 1) return { ambiguous: true, levels };
|
|
187
|
-
return { level: levels[levels.length - 1] };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
39
|
function denyNoLevelDeclared(prompt) {
|
|
191
|
-
const excerpt = prompt.slice(0, PROMPT_EXCERPT_LENGTH);
|
|
192
|
-
const ellipsis = prompt.length > PROMPT_EXCERPT_LENGTH ? '…' : '';
|
|
193
40
|
deny(
|
|
194
41
|
CONFIG_KEY,
|
|
195
|
-
`This implementation delegation ("${
|
|
42
|
+
`This implementation delegation ("${promptExcerpt(prompt)}") does not declare its risk LEVEL. Add a ` +
|
|
196
43
|
'line such as "LEVEL: STANDARD" (or QUESTION/MICRO/HIGH-RISK, whichever fits) before relaunching ' +
|
|
197
44
|
'this delegation.',
|
|
198
45
|
);
|
|
@@ -216,11 +63,16 @@ function denyLevelContradictsSignal(level, signal) {
|
|
|
216
63
|
);
|
|
217
64
|
}
|
|
218
65
|
|
|
219
|
-
function
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
66
|
+
function firstSignalText(prompt, highImpactPattern) {
|
|
67
|
+
return allMatches(highImpactPattern, stripQuoted(prompt))[0]?.[0] ?? '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isExempt(toolInput, prompt, readOnlySubagents) {
|
|
71
|
+
if (!prompt.trim()) return true;
|
|
72
|
+
if (isReadOnlySubagent(toolInput, prompt, readOnlySubagents)) return true;
|
|
73
|
+
if (isExemptQuery(prompt)) return true;
|
|
74
|
+
if (!isImplementationRequest(prompt)) return true;
|
|
75
|
+
return isDocumentaryRequest(prompt);
|
|
224
76
|
}
|
|
225
77
|
|
|
226
78
|
runGate(
|
|
@@ -237,29 +89,24 @@ runGate(
|
|
|
237
89
|
if (!toolInGroups(toolName, ['delegation'])) return;
|
|
238
90
|
|
|
239
91
|
const prompt = delegationPromptOf(toolInput);
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
92
|
+
if (isExempt(toolInput, prompt, parameters.readOnlySubagents)) return;
|
|
93
|
+
|
|
94
|
+
if (hasAmbiguousLevel(prompt)) denyAmbiguousLevel(declaredLevelsOf(prompt));
|
|
95
|
+
|
|
96
|
+
const level = operativeLevelOf(prompt);
|
|
97
|
+
if (!level) denyNoLevelDeclared(prompt.trim());
|
|
245
98
|
|
|
246
99
|
const highImpactPattern = buildHighImpactPattern(
|
|
247
100
|
parameters.highImpactPatterns,
|
|
248
101
|
);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!level) {
|
|
258
|
-
denyNoLevelDeclared(prompt.trim());
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (signal && level !== 'HIGH-RISK') {
|
|
262
|
-
denyLevelContradictsSignal(level, signal);
|
|
102
|
+
if (
|
|
103
|
+
level !== LEVELS.HIGH_RISK &&
|
|
104
|
+
hasRealSensitiveMutation(prompt, highImpactPattern)
|
|
105
|
+
) {
|
|
106
|
+
denyLevelContradictsSignal(
|
|
107
|
+
level,
|
|
108
|
+
firstSignalText(prompt, highImpactPattern),
|
|
109
|
+
);
|
|
263
110
|
}
|
|
264
111
|
},
|
|
265
112
|
);
|
|
@@ -1,23 +1,42 @@
|
|
|
1
|
+
// root-cause-first — a "patch it later" marker without a diagnosis in the same content is
|
|
2
|
+
// denied. Tests and docs are skipped: a test names the marker it checks and a doc describes
|
|
3
|
+
// the practice; neither defers a fix.
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
runGate,
|
|
3
7
|
deny,
|
|
4
8
|
toolInGroups,
|
|
5
9
|
writtenContentOf,
|
|
10
|
+
writtenPathOf,
|
|
6
11
|
delegationPromptOf,
|
|
12
|
+
compileRegexList,
|
|
7
13
|
} from '../../lib/hook-io.mjs';
|
|
14
|
+
import { isTestPath } from '../../lib/tools.mjs';
|
|
8
15
|
|
|
9
16
|
const GATE_ID = 'root-cause-first';
|
|
10
17
|
const CONFIG_KEY = 'requireRootCauseBeforePatch';
|
|
11
18
|
|
|
12
|
-
const DEFAULT_PATCH_MARKER_PATTERNS = [
|
|
19
|
+
const DEFAULT_PATCH_MARKER_PATTERNS = [
|
|
20
|
+
'todo:? fix later',
|
|
21
|
+
'fixme:? patch',
|
|
22
|
+
'hotfix',
|
|
23
|
+
'quick fix',
|
|
24
|
+
'workaround',
|
|
25
|
+
'temporary (patch|fix)',
|
|
26
|
+
'parche temporal',
|
|
27
|
+
'arreglo r[aá]pido',
|
|
28
|
+
'apa[ñn]o',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const DIAGNOSIS_PATTERN =
|
|
32
|
+
/root cause:|causa ra[ií]z:|diagnosis:|diagn[oó]stico:/i;
|
|
33
|
+
const DOCUMENT_EXTENSION_PATTERN = /\.(?:md|mdx|txt)$/i;
|
|
13
34
|
|
|
14
|
-
// The text to scan: a delegation's brief, or the content a write puts on disk. writtenContentOf
|
|
15
|
-
// covers every native and MCP write shape (Write's content, Edit's new_string, NotebookEdit's
|
|
16
|
-
// new_source, replace_file_content's new_content) — the old reader missed new_source, so a
|
|
17
|
-
// deferral marker written via NotebookEdit was never caught by this DENY gate.
|
|
18
35
|
function textToScan(toolName, toolInput) {
|
|
19
36
|
if (toolInGroups(toolName, ['delegation']))
|
|
20
37
|
return delegationPromptOf(toolInput);
|
|
38
|
+
const path = writtenPathOf(toolInput);
|
|
39
|
+
if (isTestPath(path) || DOCUMENT_EXTENSION_PATTERN.test(path)) return '';
|
|
21
40
|
return writtenContentOf(toolInput);
|
|
22
41
|
}
|
|
23
42
|
|
|
@@ -35,23 +54,21 @@ runGate(
|
|
|
35
54
|
|
|
36
55
|
const content = textToScan(toolName, toolInput);
|
|
37
56
|
if (!content) return;
|
|
57
|
+
if (DIAGNOSIS_PATTERN.test(content)) return;
|
|
38
58
|
|
|
39
|
-
const patterns = parameters.patchMarkerPatterns
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
const { patterns } = compileRegexList(parameters.patchMarkerPatterns);
|
|
60
|
+
const matched = patterns
|
|
61
|
+
.map((pattern) => pattern.exec(content))
|
|
62
|
+
.find((match) => match !== null);
|
|
43
63
|
if (!matched) return;
|
|
44
64
|
|
|
45
65
|
deny(
|
|
46
66
|
CONFIG_KEY,
|
|
47
|
-
`Content
|
|
67
|
+
`Content contains a patch-without-diagnosis marker ("${matched[0]}"). Identify the ` +
|
|
68
|
+
'root cause before patching: state it in the same content with a line starting ' +
|
|
69
|
+
'"root cause:" (or "causa raíz:", "diagnosis:", "diagnóstico:"), then retry the ' +
|
|
70
|
+
'same write. To change the markers, set patchMarkerPatterns for ' +
|
|
71
|
+
`${CONFIG_KEY} in .ai/config.json.`,
|
|
48
72
|
);
|
|
49
73
|
},
|
|
50
74
|
);
|
|
51
|
-
|
|
52
|
-
// Simplified vs. the source guard (guard-root-cause-first.mjs): the original
|
|
53
|
-
// used an auxiliary lib/embedded-content-detection.mjs module to tell real
|
|
54
|
-
// file content apart from a quoted example inside markdown. That module does
|
|
55
|
-
// not exist in this repo, so this gate matches directly against the new
|
|
56
|
-
// content/prompt. Distinguishing markdown quotes from real code is future
|
|
57
|
-
// work if false positives on quoted examples become a problem.
|