@devrik-tools/claude-gates 0.4.0 → 0.6.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.
Files changed (56) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +25 -4
  3. package/README.md +21 -5
  4. package/cli/config.mjs +126 -124
  5. package/cli/init.mjs +303 -276
  6. package/cli/install.mjs +281 -175
  7. package/cli/materialize.mjs +103 -102
  8. package/cli/registry.mjs +139 -136
  9. package/cli/smoke-fixtures.json +51 -0
  10. package/cli/task.mjs +140 -140
  11. package/package.json +1 -1
  12. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  13. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  14. package/plugins/gates/hooks/doctor.mjs +207 -207
  15. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  16. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  17. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  18. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  19. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  20. package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
  21. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  22. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  23. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  24. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  25. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  26. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  27. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  28. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  29. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  30. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  31. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  32. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  33. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  34. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  35. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  36. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  37. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  38. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  39. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  40. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  41. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  42. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  43. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  44. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  45. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  46. package/plugins/gates/hooks/hooks.json +51 -0
  47. package/plugins/gates/hooks/lib/config.mjs +179 -172
  48. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  49. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  50. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  51. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  52. package/plugins/tasks/hooks/hooks.json +26 -26
  53. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  54. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  55. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  56. package/registry.json +171 -1
@@ -1,260 +1,260 @@
1
- // intent-flow — denies an implementation delegation that has real intent to mutate a
2
- // high-impact area (money, auth, persisted data, public contract, irreversible...)
3
- // without declaring IN SCOPE / OUT OF SCOPE / EDGE CASES, and hard-denies whenever it
4
- // also leaves a high-impact UNKNOWN unresolved.
5
- //
6
- // ── How this differs from brief-before-delegate ─────────────────────────────────────
7
- // brief-before-delegate asks "was the task thought through" (goal+steps+criterion).
8
- // This gate asks a different, complementary question: "was the SCOPE declared — what
9
- // is in, what is explicitly out, and what edge cases were considered — for work that
10
- // really intends to touch a sensitive area?" Both run on the same delegation prompt
11
- // independently; neither depends on the other's verdict.
12
- //
13
- // ── Two-stage intent check (not lexical match) ──────────────────────────────────────
14
- // Stage 1 (cheap prefilter): does the raw prompt contain BOTH an implementation verb
15
- // and a high-impact signal anywhere? If not, there is no candidate — allow immediately.
16
- // Stage 2 (confirmation, only on candidates): strip quoted/templated text, then require
17
- // near co-occurrence between an implementation verb and a high-impact signal, and
18
- // discard a verb whose immediate object is a documentary deliverable (a doc/report/
19
- // README/.md/.html describing the area is not mutating it). Only surviving (a)+(b)+(c)
20
- // counts as real intent.
21
- //
22
- // ── readOnlySubagents is a declared label, not a verified capability ────────────────
23
- // This hook cannot check what tools a named subagent actually has — the whitelist
24
- // exemption is void whenever the prompt itself carries a mutation-risk signal (money/
25
- // auth/data/write/deploy): the signal in the text outranks the label on the call.
26
-
27
- import {
28
- runGate,
29
- deny,
30
- toolInGroups,
31
- delegationPromptOf,
32
- } from '../../lib/hook-io.mjs';
33
- import { MUTATION_RISK_SIGNAL } from '../../lib/signals.mjs';
34
-
35
- const GATE_ID = 'intent-flow';
36
- const CONFIG_KEY = 'requireScopeListBeforeDelegating';
37
-
38
- const DEFAULT_READ_ONLY_SUBAGENTS = ['explore', 'claude-code-guide', 'plan'];
39
- const DEFAULT_HIGH_IMPACT_PATTERNS = [
40
- 'dinero|pago|cobro|money|payment',
41
- 'auth|autenticaci[oó]n|authentication|permiso|permission|token|sesi[oó]n|session|seguridad|security|PII',
42
- 'borrar|delete|drop|migraci[oó]n|migration|schema',
43
- 'contrato|contract|irreversible|producci[oó]n|production|API p[uú]blica|public api',
44
- ];
45
-
46
- const MIN_REQUEST_LENGTH = 40;
47
- const CO_OCCURRENCE_WINDOW = 100;
48
- const VERB_OBJECT_WINDOW = 60;
49
- // How much of an over-length prompt to quote back in a denial message.
50
- const PROMPT_EXCERPT_LENGTH = 80;
51
-
52
- function withUnicodeWordBoundary(alternatives) {
53
- return new RegExp(
54
- `(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
55
- 'iu',
56
- );
57
- }
58
-
59
- // A prompt-level signal that a whitelisted read-only subagent name should NOT be
60
- // trusted to exempt this call: the name is a declared label, never a verified
61
- // capability this hook can check, and real mutation risk in the text must win over it.
62
- // Deliberately broad — over-including only means this gate's check still runs.
63
- // Centralized in lib/signals.mjs (ES+EN) — see its header for the class this covers.
64
- const MUTATION_RISK_SIGNAL_PATTERN = MUTATION_RISK_SIGNAL;
65
-
66
- const IMPLEMENTATION_VERBS = withUnicodeWordBoundary(
67
- 'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
68
- 'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|escrib(í|e) c[oó]digo|escribir c[oó]digo|' +
69
- 'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
70
- 'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
71
- 'implement|build|fix|migrate|modify|refactor|remove|rewrite|deploy',
72
- );
73
-
74
- const QUERY_VERBS = withUnicodeWordBoundary(
75
- 'explica|explic(á|ar)|qu[eé]|c[oó]mo|muestra|mostr(á|ar)|analiza|analiz(á|ar)|investiga|' +
76
- 'investig(á|ar)|revisa|revis(á|ar)|audita|audit(á|ar)|diagnostica|diagnostic(á|ar)|' +
77
- 'explain|what|how|show|analyze|investigate|review|audit|diagnose',
78
- );
79
-
80
- const IN_SCOPE_PATTERN = withUnicodeWordBoundary(
81
- 'qu[eé] s[ií]|qu[eé] s[ií] entra|alcance|in scope|lo pedido|se va a hacer|incluye',
82
- );
83
- const OUT_OF_SCOPE_PATTERN = withUnicodeWordBoundary(
84
- 'qu[eé] no|fuera de alcance|out of scope|no tocar|no modificar|no debe cambiar|' +
85
- 'must not change|no se debe (cambiar|tocar|modificar)',
86
- );
87
- const EDGE_CASES_PATTERN = withUnicodeWordBoundary(
88
- 'edge cases?|edge-cases?|casos? borde|casos? l[ií]mite|caso l[ií]mite|sin edge cases|' +
89
- 'no (hay|aplican) edge cases|no hay casos borde',
90
- );
91
- const UNRESOLVED_UNKNOWN_PATTERN = withUnicodeWordBoundary(
92
- 'unknown|no se sabe|no sabemos|sin resolver|no est[aá] claro|no est[aá] definido|' +
93
- 'a definir|por definir|desconocido|no est[aá] decidido',
94
- );
95
- const DOCUMENTARY_DELIVERABLE_PATTERN = withUnicodeWordBoundary(
96
- 'documento|documentaci[oó]n|reporte|informe|diagrama|readme|wiki|changelog|' +
97
- 'p[aá]gina de documentaci[oó]n|archivo html|p[aá]gina html|markdown|document|documentation|report',
98
- );
99
- const DOCUMENTARY_EXTENSION_PATTERN = /\.(md|html?|adoc)\b/iu;
100
-
101
- function isReadOnlySubagentName(toolInput, readOnlySubagents) {
102
- const type = String(
103
- toolInput.subagent_type ?? toolInput.subagentType ?? '',
104
- ).toLowerCase();
105
- return new Set(readOnlySubagents.map((name) => name.toLowerCase())).has(type);
106
- }
107
-
108
- /** A whitelisted subagent name exempts a call ONLY when the prompt carries no
109
- * mutation-risk signal. The name is a declared label, never a verified capability this
110
- * hook can check — a real risk signal in the text must win over it. */
111
- function isReadOnlySubagent(toolInput, prompt, readOnlySubagents) {
112
- if (!isReadOnlySubagentName(toolInput, readOnlySubagents)) return false;
113
- return !MUTATION_RISK_SIGNAL_PATTERN.test(prompt);
114
- }
115
-
116
- function isImplementationRequest(prompt) {
117
- return IMPLEMENTATION_VERBS.test(prompt);
118
- }
119
-
120
- function isExemptQuery(prompt) {
121
- if (isImplementationRequest(prompt)) return false; // implementation verb takes priority
122
- return QUERY_VERBS.test(prompt);
123
- }
124
-
125
- function stripQuoted(text) {
126
- return text
127
- .replace(/```[\s\S]*?```/g, ' ')
128
- .replace(/"[^"\n]{0,300}"/g, ' ')
129
- .replace(/'[^'\n]{0,300}'/g, ' ');
130
- }
131
-
132
- function allMatches(pattern, text) {
133
- const flags = pattern.flags.includes('g')
134
- ? pattern.flags
135
- : `${pattern.flags}g`;
136
- return [...text.matchAll(new RegExp(pattern.source, flags))];
137
- }
138
-
139
- function verbProducesDocument(text, verbMatch) {
140
- const from = verbMatch.index;
141
- const to = from + verbMatch[0].length + VERB_OBJECT_WINDOW;
142
- const objectOfVerb = text.slice(from, to);
143
- return (
144
- DOCUMENTARY_DELIVERABLE_PATTERN.test(objectOfVerb) ||
145
- DOCUMENTARY_EXTENSION_PATTERN.test(objectOfVerb)
146
- );
147
- }
148
-
149
- function hasRealSensitiveMutation(prompt, highImpactPattern) {
150
- const isCandidate =
151
- IMPLEMENTATION_VERBS.test(prompt) && highImpactPattern.test(prompt);
152
- if (!isCandidate) return false; // stage 1: cheap, no stage 2 spent
153
-
154
- const text = stripQuoted(prompt); // stage 2(a)
155
- const verbs = allMatches(IMPLEMENTATION_VERBS, text);
156
- const signals = allMatches(highImpactPattern, text);
157
-
158
- for (const verb of verbs) {
159
- if (verbProducesDocument(text, verb)) continue; // stage 2(c)
160
- for (const signal of signals) {
161
- if (Math.abs(signal.index - verb.index) <= CO_OCCURRENCE_WINDOW)
162
- return true; // stage 2(b)
163
- }
164
- }
165
- return false;
166
- }
167
-
168
- function missingSignals(prompt) {
169
- const missing = [];
170
- if (!IN_SCOPE_PATTERN.test(prompt))
171
- missing.push('IN SCOPE (what is requested, stated explicitly)');
172
- if (!OUT_OF_SCOPE_PATTERN.test(prompt))
173
- missing.push('OUT OF SCOPE (what must not be touched)');
174
- if (!EDGE_CASES_PATTERN.test(prompt)) {
175
- missing.push(
176
- 'EDGE CASES (considered, or an explicit mark that none apply)',
177
- );
178
- }
179
- return missing;
180
- }
181
-
182
- function denyUnresolvedUnknown() {
183
- deny(
184
- GATE_ID,
185
- 'This request touches a high-impact signal (money/auth/persisted data/public contract/' +
186
- 'irreversible/production) AND declares an unresolved UNKNOWN. Do not relaunch assuming an ' +
187
- 'answer — ask the user what to decide about that UNKNOWN before delegating again.',
188
- );
189
- }
190
-
191
- function denyTooShortForScopeList(prompt) {
192
- const excerpt = prompt.slice(0, PROMPT_EXCERPT_LENGTH);
193
- const ellipsis = prompt.length > PROMPT_EXCERPT_LENGTH ? '…' : '';
194
- deny(
195
- GATE_ID,
196
- `This implementation request ("${excerpt}${ellipsis}") does not carry a scope list: missing IN ` +
197
- 'SCOPE, OUT OF SCOPE and EDGE CASES. Add it to the prompt before relaunching.',
198
- );
199
- }
200
-
201
- function denyMissingSignals(missing) {
202
- deny(
203
- GATE_ID,
204
- `This implementation request does not declare, in recognizable form: ${missing.join('; ')}. ` +
205
- 'Add what is missing to the prompt before relaunching this delegation.',
206
- );
207
- }
208
-
209
- function buildHighImpactPattern(highImpactPatterns) {
210
- return new RegExp(
211
- (highImpactPatterns ?? []).map((source) => `(?:${source})`).join('|'),
212
- 'iu',
213
- );
214
- }
215
-
216
- /** True when this delegation is exempt from the whole check: not a recognizable
217
- * implementation request, a read-only subagent, or an exempt query. */
218
- function isExempt(toolInput, prompt, readOnlySubagents) {
219
- if (!prompt.trim()) return true;
220
- if (isReadOnlySubagent(toolInput, prompt, readOnlySubagents)) return true;
221
- if (isExemptQuery(prompt)) return true;
222
- return !isImplementationRequest(prompt);
223
- }
224
-
225
- runGate(
226
- {
227
- id: GATE_ID,
228
- configKey: CONFIG_KEY,
229
- enabledByDefault: false,
230
- defaultParams: {
231
- highImpactPatterns: DEFAULT_HIGH_IMPACT_PATTERNS,
232
- readOnlySubagents: DEFAULT_READ_ONLY_SUBAGENTS,
233
- },
234
- },
235
- ({ toolName, toolInput, parameters }) => {
236
- if (!toolInGroups(toolName, ['delegation'])) return;
237
-
238
- const prompt = delegationPromptOf(toolInput);
239
- if (isExempt(toolInput, prompt, parameters.readOnlySubagents)) return;
240
-
241
- const highImpactPattern = buildHighImpactPattern(
242
- parameters.highImpactPatterns,
243
- );
244
- if (!hasRealSensitiveMutation(prompt, highImpactPattern)) return;
245
-
246
- if (UNRESOLVED_UNKNOWN_PATTERN.test(stripQuoted(prompt))) {
247
- denyUnresolvedUnknown();
248
- }
249
-
250
- const trimmed = prompt.trim();
251
- if (trimmed.length < MIN_REQUEST_LENGTH) {
252
- denyTooShortForScopeList(trimmed);
253
- }
254
-
255
- const missing = missingSignals(prompt);
256
- if (missing.length === 0) return;
257
-
258
- denyMissingSignals(missing);
259
- },
260
- );
1
+ // intent-flow — denies an implementation delegation that has real intent to mutate a
2
+ // high-impact area (money, auth, persisted data, public contract, irreversible...)
3
+ // without declaring IN SCOPE / OUT OF SCOPE / EDGE CASES, and hard-denies whenever it
4
+ // also leaves a high-impact UNKNOWN unresolved.
5
+ //
6
+ // ── How this differs from brief-before-delegate ─────────────────────────────────────
7
+ // brief-before-delegate asks "was the task thought through" (goal+steps+criterion).
8
+ // This gate asks a different, complementary question: "was the SCOPE declared — what
9
+ // is in, what is explicitly out, and what edge cases were considered — for work that
10
+ // really intends to touch a sensitive area?" Both run on the same delegation prompt
11
+ // independently; neither depends on the other's verdict.
12
+ //
13
+ // ── Two-stage intent check (not lexical match) ──────────────────────────────────────
14
+ // Stage 1 (cheap prefilter): does the raw prompt contain BOTH an implementation verb
15
+ // and a high-impact signal anywhere? If not, there is no candidate — allow immediately.
16
+ // Stage 2 (confirmation, only on candidates): strip quoted/templated text, then require
17
+ // near co-occurrence between an implementation verb and a high-impact signal, and
18
+ // discard a verb whose immediate object is a documentary deliverable (a doc/report/
19
+ // README/.md/.html describing the area is not mutating it). Only surviving (a)+(b)+(c)
20
+ // counts as real intent.
21
+ //
22
+ // ── readOnlySubagents is a declared label, not a verified capability ────────────────
23
+ // This hook cannot check what tools a named subagent actually has — the whitelist
24
+ // exemption is void whenever the prompt itself carries a mutation-risk signal (money/
25
+ // auth/data/write/deploy): the signal in the text outranks the label on the call.
26
+
27
+ import {
28
+ runGate,
29
+ deny,
30
+ toolInGroups,
31
+ delegationPromptOf,
32
+ } from '../../lib/hook-io.mjs';
33
+ import { MUTATION_RISK_SIGNAL } from '../../lib/signals.mjs';
34
+
35
+ const GATE_ID = 'intent-flow';
36
+ const CONFIG_KEY = 'requireScopeListBeforeDelegating';
37
+
38
+ const DEFAULT_READ_ONLY_SUBAGENTS = ['explore', 'claude-code-guide', 'plan'];
39
+ const DEFAULT_HIGH_IMPACT_PATTERNS = [
40
+ 'dinero|pago|cobro|money|payment',
41
+ 'auth|autenticaci[oó]n|authentication|permiso|permission|token|sesi[oó]n|session|seguridad|security|PII',
42
+ 'borrar|delete|drop|migraci[oó]n|migration|schema',
43
+ 'contrato|contract|irreversible|producci[oó]n|production|API p[uú]blica|public api',
44
+ ];
45
+
46
+ const MIN_REQUEST_LENGTH = 40;
47
+ const CO_OCCURRENCE_WINDOW = 100;
48
+ const VERB_OBJECT_WINDOW = 60;
49
+ // How much of an over-length prompt to quote back in a denial message.
50
+ const PROMPT_EXCERPT_LENGTH = 80;
51
+
52
+ function withUnicodeWordBoundary(alternatives) {
53
+ return new RegExp(
54
+ `(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
55
+ 'iu',
56
+ );
57
+ }
58
+
59
+ // A prompt-level signal that a whitelisted read-only subagent name should NOT be
60
+ // trusted to exempt this call: the name is a declared label, never a verified
61
+ // capability this hook can check, and real mutation risk in the text must win over it.
62
+ // Deliberately broad — over-including only means this gate's check still runs.
63
+ // Centralized in lib/signals.mjs (ES+EN) — see its header for the class this covers.
64
+ const MUTATION_RISK_SIGNAL_PATTERN = MUTATION_RISK_SIGNAL;
65
+
66
+ const IMPLEMENTATION_VERBS = withUnicodeWordBoundary(
67
+ 'implementa|implementar|implement(á|é)|agreg(a|á)|agregar|añad(e|í)|añadir|cre(a|á)|crear|' +
68
+ 'arregl(a|á)|arreglar|cambi(a|á)|cambiar|migr(a|á)|migrar|escrib(í|e) c[oó]digo|escribir c[oó]digo|' +
69
+ 'corrige|corregir|correg(í|ir)|constru(ye|í)|construir|modific(a|á)|modificar|' +
70
+ 'refactoriz(a|á)|refactorizar|elimin(a|á)|eliminar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
71
+ 'implement|build|fix|migrate|modify|refactor|remove|rewrite|deploy',
72
+ );
73
+
74
+ const QUERY_VERBS = withUnicodeWordBoundary(
75
+ 'explica|explic(á|ar)|qu[eé]|c[oó]mo|muestra|mostr(á|ar)|analiza|analiz(á|ar)|investiga|' +
76
+ 'investig(á|ar)|revisa|revis(á|ar)|audita|audit(á|ar)|diagnostica|diagnostic(á|ar)|' +
77
+ 'explain|what|how|show|analyze|investigate|review|audit|diagnose',
78
+ );
79
+
80
+ const IN_SCOPE_PATTERN = withUnicodeWordBoundary(
81
+ 'qu[eé] s[ií]|qu[eé] s[ií] entra|alcance|in scope|lo pedido|se va a hacer|incluye',
82
+ );
83
+ const OUT_OF_SCOPE_PATTERN = withUnicodeWordBoundary(
84
+ 'qu[eé] no|fuera de alcance|out of scope|no tocar|no modificar|no debe cambiar|' +
85
+ 'must not change|no se debe (cambiar|tocar|modificar)',
86
+ );
87
+ const EDGE_CASES_PATTERN = withUnicodeWordBoundary(
88
+ 'edge cases?|edge-cases?|casos? borde|casos? l[ií]mite|caso l[ií]mite|sin edge cases|' +
89
+ 'no (hay|aplican) edge cases|no hay casos borde',
90
+ );
91
+ const UNRESOLVED_UNKNOWN_PATTERN = withUnicodeWordBoundary(
92
+ 'unknown|no se sabe|no sabemos|sin resolver|no est[aá] claro|no est[aá] definido|' +
93
+ 'a definir|por definir|desconocido|no est[aá] decidido',
94
+ );
95
+ const DOCUMENTARY_DELIVERABLE_PATTERN = withUnicodeWordBoundary(
96
+ 'documento|documentaci[oó]n|reporte|informe|diagrama|readme|wiki|changelog|' +
97
+ 'p[aá]gina de documentaci[oó]n|archivo html|p[aá]gina html|markdown|document|documentation|report',
98
+ );
99
+ const DOCUMENTARY_EXTENSION_PATTERN = /\.(md|html?|adoc)\b/iu;
100
+
101
+ function isReadOnlySubagentName(toolInput, readOnlySubagents) {
102
+ const type = String(
103
+ toolInput.subagent_type ?? toolInput.subagentType ?? '',
104
+ ).toLowerCase();
105
+ return new Set(readOnlySubagents.map((name) => name.toLowerCase())).has(type);
106
+ }
107
+
108
+ /** A whitelisted subagent name exempts a call ONLY when the prompt carries no
109
+ * mutation-risk signal. The name is a declared label, never a verified capability this
110
+ * hook can check — a real risk signal in the text must win over it. */
111
+ function isReadOnlySubagent(toolInput, prompt, readOnlySubagents) {
112
+ if (!isReadOnlySubagentName(toolInput, readOnlySubagents)) return false;
113
+ return !MUTATION_RISK_SIGNAL_PATTERN.test(prompt);
114
+ }
115
+
116
+ function isImplementationRequest(prompt) {
117
+ return IMPLEMENTATION_VERBS.test(prompt);
118
+ }
119
+
120
+ function isExemptQuery(prompt) {
121
+ if (isImplementationRequest(prompt)) return false; // implementation verb takes priority
122
+ return QUERY_VERBS.test(prompt);
123
+ }
124
+
125
+ function stripQuoted(text) {
126
+ return text
127
+ .replace(/```[\s\S]*?```/g, ' ')
128
+ .replace(/"[^"\n]{0,300}"/g, ' ')
129
+ .replace(/'[^'\n]{0,300}'/g, ' ');
130
+ }
131
+
132
+ function allMatches(pattern, text) {
133
+ const flags = pattern.flags.includes('g')
134
+ ? pattern.flags
135
+ : `${pattern.flags}g`;
136
+ return [...text.matchAll(new RegExp(pattern.source, flags))];
137
+ }
138
+
139
+ function verbProducesDocument(text, verbMatch) {
140
+ const from = verbMatch.index;
141
+ const to = from + verbMatch[0].length + VERB_OBJECT_WINDOW;
142
+ const objectOfVerb = text.slice(from, to);
143
+ return (
144
+ DOCUMENTARY_DELIVERABLE_PATTERN.test(objectOfVerb) ||
145
+ DOCUMENTARY_EXTENSION_PATTERN.test(objectOfVerb)
146
+ );
147
+ }
148
+
149
+ function hasRealSensitiveMutation(prompt, highImpactPattern) {
150
+ const isCandidate =
151
+ IMPLEMENTATION_VERBS.test(prompt) && highImpactPattern.test(prompt);
152
+ if (!isCandidate) return false; // stage 1: cheap, no stage 2 spent
153
+
154
+ const text = stripQuoted(prompt); // stage 2(a)
155
+ const verbs = allMatches(IMPLEMENTATION_VERBS, text);
156
+ const signals = allMatches(highImpactPattern, text);
157
+
158
+ for (const verb of verbs) {
159
+ if (verbProducesDocument(text, verb)) continue; // stage 2(c)
160
+ for (const signal of signals) {
161
+ if (Math.abs(signal.index - verb.index) <= CO_OCCURRENCE_WINDOW)
162
+ return true; // stage 2(b)
163
+ }
164
+ }
165
+ return false;
166
+ }
167
+
168
+ function missingSignals(prompt) {
169
+ const missing = [];
170
+ if (!IN_SCOPE_PATTERN.test(prompt))
171
+ missing.push('IN SCOPE (what is requested, stated explicitly)');
172
+ if (!OUT_OF_SCOPE_PATTERN.test(prompt))
173
+ missing.push('OUT OF SCOPE (what must not be touched)');
174
+ if (!EDGE_CASES_PATTERN.test(prompt)) {
175
+ missing.push(
176
+ 'EDGE CASES (considered, or an explicit mark that none apply)',
177
+ );
178
+ }
179
+ return missing;
180
+ }
181
+
182
+ function denyUnresolvedUnknown() {
183
+ deny(
184
+ GATE_ID,
185
+ 'This request touches a high-impact signal (money/auth/persisted data/public contract/' +
186
+ 'irreversible/production) AND declares an unresolved UNKNOWN. Do not relaunch assuming an ' +
187
+ 'answer — ask the user what to decide about that UNKNOWN before delegating again.',
188
+ );
189
+ }
190
+
191
+ function denyTooShortForScopeList(prompt) {
192
+ const excerpt = prompt.slice(0, PROMPT_EXCERPT_LENGTH);
193
+ const ellipsis = prompt.length > PROMPT_EXCERPT_LENGTH ? '…' : '';
194
+ deny(
195
+ GATE_ID,
196
+ `This implementation request ("${excerpt}${ellipsis}") does not carry a scope list: missing IN ` +
197
+ 'SCOPE, OUT OF SCOPE and EDGE CASES. Add it to the prompt before relaunching.',
198
+ );
199
+ }
200
+
201
+ function denyMissingSignals(missing) {
202
+ deny(
203
+ GATE_ID,
204
+ `This implementation request does not declare, in recognizable form: ${missing.join('; ')}. ` +
205
+ 'Add what is missing to the prompt before relaunching this delegation.',
206
+ );
207
+ }
208
+
209
+ function buildHighImpactPattern(highImpactPatterns) {
210
+ return new RegExp(
211
+ (highImpactPatterns ?? []).map((source) => `(?:${source})`).join('|'),
212
+ 'iu',
213
+ );
214
+ }
215
+
216
+ /** True when this delegation is exempt from the whole check: not a recognizable
217
+ * implementation request, a read-only subagent, or an exempt query. */
218
+ function isExempt(toolInput, prompt, readOnlySubagents) {
219
+ if (!prompt.trim()) return true;
220
+ if (isReadOnlySubagent(toolInput, prompt, readOnlySubagents)) return true;
221
+ if (isExemptQuery(prompt)) return true;
222
+ return !isImplementationRequest(prompt);
223
+ }
224
+
225
+ runGate(
226
+ {
227
+ id: GATE_ID,
228
+ configKey: CONFIG_KEY,
229
+ enabledByDefault: false,
230
+ defaultParams: {
231
+ highImpactPatterns: DEFAULT_HIGH_IMPACT_PATTERNS,
232
+ readOnlySubagents: DEFAULT_READ_ONLY_SUBAGENTS,
233
+ },
234
+ },
235
+ ({ toolName, toolInput, parameters }) => {
236
+ if (!toolInGroups(toolName, ['delegation'])) return;
237
+
238
+ const prompt = delegationPromptOf(toolInput);
239
+ if (isExempt(toolInput, prompt, parameters.readOnlySubagents)) return;
240
+
241
+ const highImpactPattern = buildHighImpactPattern(
242
+ parameters.highImpactPatterns,
243
+ );
244
+ if (!hasRealSensitiveMutation(prompt, highImpactPattern)) return;
245
+
246
+ if (UNRESOLVED_UNKNOWN_PATTERN.test(stripQuoted(prompt))) {
247
+ denyUnresolvedUnknown();
248
+ }
249
+
250
+ const trimmed = prompt.trim();
251
+ if (trimmed.length < MIN_REQUEST_LENGTH) {
252
+ denyTooShortForScopeList(trimmed);
253
+ }
254
+
255
+ const missing = missingSignals(prompt);
256
+ if (missing.length === 0) return;
257
+
258
+ denyMissingSignals(missing);
259
+ },
260
+ );