@devrik-tools/claude-gates 0.1.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 (80) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.es.md +219 -0
  3. package/README.md +222 -0
  4. package/cli/__tests__/config.test.mjs +101 -0
  5. package/cli/__tests__/init-flags.test.mjs +111 -0
  6. package/cli/__tests__/registry-gates-consistency.test.mjs +44 -0
  7. package/cli/__tests__/selection.test.mjs +200 -0
  8. package/cli/config.mjs +113 -0
  9. package/cli/constants.mjs +36 -0
  10. package/cli/index.mjs +97 -0
  11. package/cli/init.mjs +256 -0
  12. package/cli/install.mjs +80 -0
  13. package/cli/materialize.mjs +51 -0
  14. package/cli/registry.mjs +127 -0
  15. package/cli/selection.mjs +83 -0
  16. package/package.json +73 -0
  17. package/plugins/gates/.claude-plugin/plugin.json +9 -0
  18. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -0
  19. package/plugins/gates/hooks/gates/audit-before-build/test.mjs +92 -0
  20. package/plugins/gates/hooks/gates/bash-commands/index.mjs +233 -0
  21. package/plugins/gates/hooks/gates/bash-commands/test.mjs +113 -0
  22. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +177 -0
  23. package/plugins/gates/hooks/gates/brief-before-delegate/test.mjs +111 -0
  24. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +456 -0
  25. package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +143 -0
  26. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +118 -0
  27. package/plugins/gates/hooks/gates/dependency-skills/test.mjs +109 -0
  28. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +49 -0
  29. package/plugins/gates/hooks/gates/diagnosis-before-patch/test.mjs +68 -0
  30. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +100 -0
  31. package/plugins/gates/hooks/gates/feature-catalog/test.mjs +97 -0
  32. package/plugins/gates/hooks/gates/forge-flow/index.mjs +112 -0
  33. package/plugins/gates/hooks/gates/forge-flow/test.mjs +135 -0
  34. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +186 -0
  35. package/plugins/gates/hooks/gates/implementation-pipeline/test.mjs +86 -0
  36. package/plugins/gates/hooks/gates/intent-flow/index.mjs +238 -0
  37. package/plugins/gates/hooks/gates/intent-flow/test.mjs +136 -0
  38. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +166 -0
  39. package/plugins/gates/hooks/gates/mandatory-flow/test.mjs +119 -0
  40. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +85 -0
  41. package/plugins/gates/hooks/gates/neutral-spanish/test.mjs +65 -0
  42. package/plugins/gates/hooks/gates/never-assume/index.mjs +55 -0
  43. package/plugins/gates/hooks/gates/never-assume/test.mjs +78 -0
  44. package/plugins/gates/hooks/gates/no-blocking/index.mjs +142 -0
  45. package/plugins/gates/hooks/gates/no-blocking/test.mjs +108 -0
  46. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +120 -0
  47. package/plugins/gates/hooks/gates/no-memory-dependency/test.mjs +106 -0
  48. package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +202 -0
  49. package/plugins/gates/hooks/gates/no-reconfirm/test.mjs +131 -0
  50. package/plugins/gates/hooks/gates/protected-paths/index.mjs +147 -0
  51. package/plugins/gates/hooks/gates/protected-paths/test.mjs +75 -0
  52. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +64 -0
  53. package/plugins/gates/hooks/gates/recurrence-lock/test.mjs +99 -0
  54. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +150 -0
  55. package/plugins/gates/hooks/gates/reuse-before-build/test.mjs +101 -0
  56. package/plugins/gates/hooks/gates/risk-level/index.mjs +203 -0
  57. package/plugins/gates/hooks/gates/risk-level/test.mjs +125 -0
  58. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +50 -0
  59. package/plugins/gates/hooks/gates/root-cause-first/test.mjs +73 -0
  60. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +111 -0
  61. package/plugins/gates/hooks/gates/root-whitelist/test.mjs +80 -0
  62. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +99 -0
  63. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/test.mjs +78 -0
  64. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +251 -0
  65. package/plugins/gates/hooks/gates/sdd-specs/test.mjs +163 -0
  66. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -0
  67. package/plugins/gates/hooks/gates/test-after-implementation/test.mjs +81 -0
  68. package/plugins/gates/hooks/gates/test-matrix/index.mjs +181 -0
  69. package/plugins/gates/hooks/gates/test-matrix/test.mjs +87 -0
  70. package/plugins/gates/hooks/gates/tool-map/index.mjs +140 -0
  71. package/plugins/gates/hooks/gates/tool-map/test.mjs +87 -0
  72. package/plugins/gates/hooks/hooks.json +266 -0
  73. package/plugins/gates/hooks/lib/__tests__/config.test.mjs +154 -0
  74. package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +154 -0
  75. package/plugins/gates/hooks/lib/config.mjs +165 -0
  76. package/plugins/gates/hooks/lib/hook-io.mjs +208 -0
  77. package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +132 -0
  78. package/plugins/tasks/hooks/lib/task-store.mjs +159 -0
  79. package/plugins/tasks/hooks/register-requests.mjs +108 -0
  80. package/registry.json +668 -0
@@ -0,0 +1,177 @@
1
+ // brief-before-delegate — denies an implementation delegation whose prompt does not
2
+ // declare, in recognizable form, a GOAL, concrete STEPS and a DONE-WHEN criterion.
3
+ //
4
+ // ── Why this is decidable, and what it deliberately does not judge ─────────────────
5
+ // A hook sees a tool call, not how hard the underlying task is. "Complex" is not a
6
+ // fact in the payload, and any heuristic that approximates it (file count, prompt
7
+ // length, "architecture" keywords) ends up blocking trivial work sooner or later.
8
+ // The decidable question is narrower: did THIS delegation's prompt state a goal, its
9
+ // steps and a success criterion before the subagent starts? That is a fact about the
10
+ // text itself, not a judgment about the task.
11
+ //
12
+ // Whether the declared goal is the RIGHT goal, or the steps are the RIGHT steps, or
13
+ // the criterion truly resolves the ambiguity — that needs domain understanding a
14
+ // script cannot supply. This gate only enforces that the brief EXISTS, never that it
15
+ // is good.
16
+ //
17
+ // ── What is exempt ───────────────────────────────────────────────────────────────
18
+ // - Read-only exploration/subagents (readOnlySubagents param, or a prompt whose
19
+ // dominant verb is investigate/search/read/explain/audit with no implementation
20
+ // verb): that is a QUESTION, not an implementation order — forcing a brief onto it
21
+ // would make it simulate project structure it does not have.
22
+ // - A prompt that already carries structure (a list, numbered steps, or prose that
23
+ // otherwise states the three signals): the form is free, only the content is
24
+ // required.
25
+
26
+ import { runGate, deny, warn, TOOL_GROUPS } from '../../lib/hook-io.mjs';
27
+
28
+ const GATE_ID = 'brief-before-delegate';
29
+ const CONFIG_KEY = 'requireBriefBeforeDelegating';
30
+
31
+ const DELEGATION_TOOLS = new Set(TOOL_GROUPS.delegation);
32
+
33
+ const DEFAULT_MIN_BRIEF_LENGTH = 180;
34
+ const DEFAULT_READ_ONLY_SUBAGENTS = ['explore', 'claude-code-guide', 'plan'];
35
+
36
+ // How much of an over-length prompt to quote back in a denial message.
37
+ const PROMPT_EXCERPT_LENGTH = 80;
38
+ // Total signals this gate checks for (goal, steps, criterion): when all are missing at
39
+ // once it is indistinguishable from "never thought through", so that combination denies
40
+ // instead of only warning.
41
+ const TOTAL_REQUIRED_SIGNALS = 3;
42
+
43
+ /** Unicode-aware word boundary: JS's `\b` does not treat accented letters as word
44
+ * chars, so a plain `\bcorregi\b`-style pattern silently misses an accented
45
+ * imperative. Lookarounds over `\p{L}|\p{N}|_` cover the full alphabet instead. */
46
+ function withUnicodeWordBoundary(alternatives) {
47
+ return new RegExp(
48
+ `(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
49
+ 'iu',
50
+ );
51
+ }
52
+
53
+ const IMPLEMENTATION_VERBS = withUnicodeWordBoundary(
54
+ 'implementa|implementar|escrib(e|í)|escribir|cre(a|á)|crear|corrige|correg(í|ir)|' +
55
+ 'arregl(a|á)|arreglar|constru(ye|í)|construir|refactoriz(a|á)|refactorizar|' +
56
+ 'migr(a|á)|migrar|agreg(a|á)|agregar|añad(e|í)|añadir|elimin(a|á)|eliminar|' +
57
+ 'modific(a|á)|modificar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
58
+ 'hac(e|er|é)|resuelve|resolv(é|er)|soluciona|solucion(á|ar)|encárgate|encargate|' +
59
+ 'ocúpate|ocupate|cambi(a|á)|cambiar|actualiz(a|á)|actualizar|' +
60
+ 'implement|write|fix|build|refactor|migrate|add|remove|modify|update',
61
+ );
62
+
63
+ const READ_ONLY_VERBS = withUnicodeWordBoundary(
64
+ 'investiga|investigar|busca|buscar|explora|explorar|lee|leer|explica|explicar|' +
65
+ 'audita|auditar|analiza|analizar|compara|comparar|diagnostica|diagnosticar|' +
66
+ 'revisa|revisar|averigua|averiguar|' +
67
+ 'investigate|search|explore|read|explain|audit|analyze|compare|diagnose|review',
68
+ );
69
+
70
+ /** Evidence of a stated GOAL. */
71
+ const GOAL_PATTERN = withUnicodeWordBoundary(
72
+ 'objetivo|meta|el fin es|se busca|para lograr|para que|goal|objective',
73
+ );
74
+
75
+ /** Evidence of structured STEPS: a list/bullet/numbered form. Anchored per-line (`m`
76
+ * flag) with a single bounded `\s*` after the line start, so there is no nested
77
+ * quantifier for a backtracking engine to explode on. */
78
+ const STRUCTURED_STEPS_PATTERN = /^[ \t]*(?:[-*•]|\d+[.)])[ \t]+\S/m;
79
+
80
+ /** Evidence of a DONE-WHEN / acceptance criterion. */
81
+ const CRITERION_PATTERN = withUnicodeWordBoundary(
82
+ 'criterio|acceptance|asserts?|se considera (hecho|terminado|listo)|' +
83
+ 'debe (verificarse|cumplir|pasar)|hasta que|done.when|' +
84
+ 'cuando (esto|el) (pase|funcione)|dado.{0,20}cuando.{0,20}entonces|' +
85
+ 'given.{0,20}when.{0,20}then',
86
+ );
87
+
88
+ function isReadOnlySubagent(toolInput, readOnlySubagents) {
89
+ const type = String(
90
+ toolInput.subagent_type ?? toolInput.subagentType ?? '',
91
+ ).toLowerCase();
92
+ return new Set(readOnlySubagents.map((name) => name.toLowerCase())).has(type);
93
+ }
94
+
95
+ function isReadOnlyRequest(prompt) {
96
+ if (IMPLEMENTATION_VERBS.test(prompt)) return false;
97
+ return READ_ONLY_VERBS.test(prompt);
98
+ }
99
+
100
+ function missingSignals(prompt) {
101
+ const missing = [];
102
+ if (!GOAL_PATTERN.test(prompt))
103
+ missing.push('a stated GOAL (what this aims to achieve)');
104
+ if (!STRUCTURED_STEPS_PATTERN.test(prompt)) {
105
+ missing.push('STEPS as a list or numbered form (concrete files/actions)');
106
+ }
107
+ if (!CRITERION_PATTERN.test(prompt)) {
108
+ missing.push(
109
+ 'a DONE-WHEN / acceptance criterion (how completion is verified)',
110
+ );
111
+ }
112
+ return missing;
113
+ }
114
+
115
+ function denyTooShort(prompt) {
116
+ const excerpt = prompt.slice(0, PROMPT_EXCERPT_LENGTH);
117
+ const ellipsis = prompt.length > PROMPT_EXCERPT_LENGTH ? '…' : '';
118
+ deny(
119
+ GATE_ID,
120
+ `This delegation asks for implementation ("${excerpt}${ellipsis}") in a ${prompt.length}-character ` +
121
+ 'prompt — too short to carry a goal, steps and a done-when criterion. State what this aims to ' +
122
+ 'achieve, what concretely needs doing (as a list or steps), and how completion is verified, then ' +
123
+ 'relaunch.',
124
+ );
125
+ }
126
+
127
+ function reportMissingSignals(missing) {
128
+ if (missing.length === 0) return;
129
+
130
+ if (missing.length < TOTAL_REQUIRED_SIGNALS) {
131
+ warn(
132
+ GATE_ID,
133
+ `This implementation delegation does not recognizably state: ${missing.join('; ')}. ` +
134
+ 'If it is already there under different wording, proceed — this is only a warning. ' +
135
+ 'Otherwise add it before the subagent starts blind.',
136
+ );
137
+ return;
138
+ }
139
+
140
+ deny(
141
+ GATE_ID,
142
+ 'This delegation asks for implementation but states neither as a list nor recognizable prose: ' +
143
+ `${missing.join('; ')}. Add to the prompt: (1) the GOAL — what this aims to achieve; ` +
144
+ '(2) the STEPS — concrete files/actions, as a list; (3) the CRITERION — how completion is ' +
145
+ 'verified. The form is free; the content is not optional.',
146
+ );
147
+ }
148
+
149
+ runGate(
150
+ {
151
+ id: GATE_ID,
152
+ configKey: CONFIG_KEY,
153
+ enabledByDefault: false,
154
+ defaultParams: {
155
+ minBriefLength: DEFAULT_MIN_BRIEF_LENGTH,
156
+ readOnlySubagents: DEFAULT_READ_ONLY_SUBAGENTS,
157
+ },
158
+ },
159
+ ({ toolName, toolInput, parameters }) => {
160
+ if (!DELEGATION_TOOLS.has(toolName)) return;
161
+
162
+ const prompt = String(
163
+ toolInput.prompt ?? toolInput.description ?? toolInput.task ?? '',
164
+ );
165
+ if (!prompt.trim()) return;
166
+ if (isReadOnlySubagent(toolInput, parameters.readOnlySubagents)) return;
167
+ if (isReadOnlyRequest(prompt)) return;
168
+ if (!IMPLEMENTATION_VERBS.test(prompt)) return; // neither implementation nor read-only: do not guess
169
+
170
+ const trimmed = prompt.trim();
171
+ if (trimmed.length < parameters.minBriefLength) {
172
+ denyTooShort(trimmed);
173
+ }
174
+
175
+ reportMissingSignals(missingSignals(prompt));
176
+ },
177
+ );
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { test } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
10
+
11
+ function runGate(payload, { config } = {}) {
12
+ const project = mkdtempSync(join(tmpdir(), 'brief-before-delegate-'));
13
+ mkdirSync(join(project, '.git'));
14
+ if (config) {
15
+ mkdirSync(join(project, '.ai'));
16
+ writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
17
+ }
18
+ const out = execFileSync(process.execPath, [GATE], {
19
+ input: JSON.stringify(payload),
20
+ encoding: 'utf8',
21
+ cwd: project,
22
+ // Isolate from the user's real global config: point homedir() at the temp
23
+ // project so the global-config fallback finds nothing (registry default).
24
+ env: { ...process.env, HOME: project, USERPROFILE: project },
25
+ });
26
+ return out.trim() ? JSON.parse(out.trim()) : null;
27
+ }
28
+
29
+ function delegate(prompt, extra = {}) {
30
+ return { tool_name: 'Agent', tool_input: { prompt, ...extra } };
31
+ }
32
+ function isDeny(result) {
33
+ return result?.hookSpecificOutput?.permissionDecision === 'deny';
34
+ }
35
+ const ENABLED = { config: { gates: { requireBriefBeforeDelegating: true } } };
36
+
37
+ test('denies an implementation delegation with no goal/steps/criterion', () => {
38
+ const result = runGate(
39
+ delegate('Fix the login bug please, thanks.'),
40
+ ENABLED,
41
+ );
42
+ assert.ok(isDeny(result));
43
+ });
44
+
45
+ test('allows a complete brief with goal, steps and done-when criterion', () => {
46
+ const prompt = [
47
+ 'Objetivo: fix the broken login redirect so users land on the dashboard.',
48
+ '',
49
+ 'Haceres:',
50
+ '- update src/auth/redirect.js to use the post-login route',
51
+ '- add a regression test for the redirect',
52
+ '',
53
+ 'Criterio: se considera hecho cuando el test de regresion pasa y el login redirige correctamente.',
54
+ ].join('\n');
55
+ assert.equal(runGate(delegate(prompt), ENABLED), null);
56
+ });
57
+
58
+ test('allows a read-only exploration prompt without a brief', () => {
59
+ assert.equal(
60
+ runGate(
61
+ delegate('Investiga donde esta definida la funcion de login.'),
62
+ ENABLED,
63
+ ),
64
+ null,
65
+ );
66
+ });
67
+
68
+ test('allows a read-only subagent even with an implementation verb', () => {
69
+ assert.equal(
70
+ runGate(
71
+ delegate('Implementa un resumen de como arreglar el login.', {
72
+ subagent_type: 'explore',
73
+ }),
74
+ ENABLED,
75
+ ),
76
+ null,
77
+ );
78
+ });
79
+
80
+ test('disabled by config: the gate does not run', () => {
81
+ assert.equal(
82
+ runGate(delegate('Fix the login bug please, thanks.'), {
83
+ config: { gates: { requireBriefBeforeDelegating: false } },
84
+ }),
85
+ null,
86
+ );
87
+ });
88
+
89
+ test('off by default when config is silent', () => {
90
+ assert.equal(runGate(delegate('Fix the login bug please, thanks.')), null);
91
+ });
92
+
93
+ test('project minBriefLength override changes the length threshold', () => {
94
+ const config = {
95
+ gates: {
96
+ requireBriefBeforeDelegating: { enabled: true, minBriefLength: 20 },
97
+ },
98
+ };
99
+ const prompt =
100
+ 'Fix the login bug in the auth module now, using the same approach as before.';
101
+ // With the default 180-char minimum, this short prompt is denied outright for length.
102
+ assert.ok(isDeny(runGate(delegate(prompt), ENABLED)));
103
+ // With the length floor lowered to 20, it clears the length check. It still misses
104
+ // all three signals, which is its own hard-deny path — but the reason changes: this
105
+ // proves the override changed the length check specifically, by using a prompt that
106
+ // is long enough that only two signals are missing (a goal is present), which is a
107
+ // warn rather than a deny.
108
+ const withGoal = `Goal: ${prompt}`;
109
+ const result = runGate(delegate(withGoal), { config });
110
+ assert.ok(!isDeny(result));
111
+ });