@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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.es.md +129 -11
  3. package/README.md +120 -10
  4. package/cli/doctor.mjs +132 -0
  5. package/cli/evidence.mjs +78 -0
  6. package/cli/hooks-manifest.mjs +89 -0
  7. package/cli/index.mjs +124 -6
  8. package/cli/init.mjs +97 -3
  9. package/cli/log.mjs +70 -0
  10. package/cli/materialize.mjs +36 -2
  11. package/cli/registry.mjs +12 -1
  12. package/cli/selection.mjs +18 -0
  13. package/cli/smoke-fixtures.json +159 -25
  14. package/cli/task.mjs +104 -11
  15. package/cli/toggle.mjs +125 -0
  16. package/package.json +4 -3
  17. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  18. package/plugins/gates/hooks/doctor.mjs +47 -1
  19. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
  20. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
  21. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
  22. package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
  23. package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
  24. package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
  25. package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
  26. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
  27. package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
  28. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +94 -215
  29. package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
  30. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
  31. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
  32. package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
  33. package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
  34. package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
  35. package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
  36. package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
  37. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
  38. package/plugins/gates/hooks/gates/force-parallel/index.mjs +53 -95
  39. package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
  40. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
  41. package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
  42. package/plugins/gates/hooks/gates/library-docs/index.mjs +338 -0
  43. package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
  44. package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
  45. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
  46. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
  47. package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
  48. package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
  49. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
  50. package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
  51. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
  52. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
  53. package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
  54. package/plugins/gates/hooks/gates/no-trivial-scripts/index.mjs +114 -0
  55. package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
  56. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
  57. package/plugins/gates/hooks/gates/require-monitor/index.mjs +126 -0
  58. package/plugins/gates/hooks/gates/require-task-split/index.mjs +88 -0
  59. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
  60. package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
  61. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
  62. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
  63. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
  64. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
  65. package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
  66. package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
  67. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
  68. package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
  69. package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
  70. package/plugins/gates/hooks/hooks.json +152 -42
  71. package/plugins/gates/hooks/lib/config.mjs +6 -3
  72. package/plugins/gates/hooks/lib/delegation.mjs +352 -0
  73. package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
  74. package/plugins/gates/hooks/lib/git.mjs +262 -0
  75. package/plugins/gates/hooks/lib/hook-io.mjs +451 -83
  76. package/plugins/gates/hooks/lib/session-state.mjs +150 -0
  77. package/plugins/gates/hooks/lib/signals.mjs +18 -13
  78. package/plugins/gates/hooks/lib/testing.mjs +148 -0
  79. package/plugins/gates/hooks/lib/tools.mjs +144 -0
  80. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  81. package/plugins/tasks/hooks/lib/task-store.mjs +12 -6
  82. package/plugins/tasks/hooks/register-requests.mjs +37 -10
  83. package/registry.json +249 -38
@@ -1,33 +1,20 @@
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.
1
+ // brief-before-delegate — denies an implementation delegation whose prompt does not state a
2
+ // GOAL, concrete STEPS and a DONE-WHEN criterion, each with real content past its marker.
3
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. It DOES require each section to carry actual content past its marker —
16
- // pasting the words "Objetivo:"/"Criterio:" next to filler is not a brief either.
17
- //
18
- // ── What is exempt ───────────────────────────────────────────────────────────────
19
- // - Read-only exploration/subagents (readOnlySubagents param, or a prompt whose
20
- // dominant verb is investigate/search/read/explain/audit with no implementation
21
- // verb): that is a QUESTION, not an implementation order — forcing a brief onto it
22
- // would make it simulate project structure it does not have.
23
- // The readOnlySubagents exemption is a name the delegator declares, not a verified
24
- // capability this hook can check — so it is void whenever the prompt itself carries
25
- // a mutation-risk signal (money/auth/data/write/deploy): the signal in the text
26
- // outranks the label on the call.
27
- // - A prompt that already carries structure (a list, numbered steps, or prose that
28
- // otherwise states the three signals): the form is free, only the content is
29
- // required.
4
+ // Decisions: the gate judges that the brief EXISTS, never that it is good "is this the
5
+ // right goal" needs domain judgment a hook cannot supply. A read-only subagent name is a
6
+ // declared label, not a verified capability, so it exempts only while the prompt carries no
7
+ // mutation-risk signal. A documentary deliverable (README, report, changelog) is not an
8
+ // implementation brief and is not judged here.
30
9
 
10
+ import {
11
+ DEFAULT_READ_ONLY_SUBAGENTS,
12
+ isDocumentaryRequest,
13
+ isExemptQuery,
14
+ isImplementationRequest,
15
+ isReadOnlySubagent,
16
+ promptExcerpt,
17
+ } from '../../lib/delegation.mjs';
31
18
  import {
32
19
  runGate,
33
20
  deny,
@@ -35,71 +22,29 @@ import {
35
22
  toolInGroups,
36
23
  delegationPromptOf,
37
24
  } from '../../lib/hook-io.mjs';
38
- import { MUTATION_RISK_SIGNAL } from '../../lib/signals.mjs';
25
+ import { withUnicodeWordBoundary } from '../../lib/signals.mjs';
39
26
 
40
27
  const GATE_ID = 'brief-before-delegate';
41
28
  const CONFIG_KEY = 'requireBriefBeforeDelegating';
42
29
 
43
30
  const DEFAULT_MIN_BRIEF_LENGTH = 180;
44
- const DEFAULT_READ_ONLY_SUBAGENTS = ['explore', 'claude-code-guide', 'plan'];
45
31
 
46
- // How much of an over-length prompt to quote back in a denial message.
47
- const PROMPT_EXCERPT_LENGTH = 80;
48
- // Total signals this gate checks for (goal, steps, criterion): when all are missing at
49
- // once it is indistinguishable from "never thought through", so that combination denies
50
- // instead of only warning.
32
+ // All three signals missing at once is indistinguishable from "never thought through", so
33
+ // that combination denies where a partial brief only warns.
51
34
  const TOTAL_REQUIRED_SIGNALS = 3;
52
- // Minimum substantial (non-stopword) characters a section needs past its own marker to
53
- // count as "stated" instead of merely name-dropped. Chosen so a single short filler
54
- // word ("cosa.", "listo.") does not clear it, but any real sentence does.
35
+ // A single filler word ("cosa.", "listo.") must not clear the bar; any real sentence does.
55
36
  const MIN_SECTION_SUBSTANCE_LENGTH = 12;
37
+ const MIN_SUBSTANTIVE_WORDS = 2;
38
+ const MIN_WORD_LENGTH = 3;
56
39
 
57
- /** Unicode-aware word boundary: JS's `\b` does not treat accented letters as word
58
- * chars, so a plain `\bcorregi\b`-style pattern silently misses an accented
59
- * imperative. Lookarounds over `\p{L}|\p{N}|_` cover the full alphabet instead. */
60
- function withUnicodeWordBoundary(alternatives) {
61
- return new RegExp(
62
- `(?<![\\p{L}\\p{N}_])(${alternatives})(?![\\p{L}\\p{N}_])`,
63
- 'iu',
64
- );
65
- }
66
-
67
- const IMPLEMENTATION_VERBS = withUnicodeWordBoundary(
68
- 'implementa|implementar|escrib(e|í)|escribir|cre(a|á)|crear|corrige|correg(í|ir)|' +
69
- 'arregl(a|á)|arreglar|constru(ye|í)|construir|refactoriz(a|á)|refactorizar|' +
70
- 'migr(a|á)|migrar|agreg(a|á)|agregar|añad(e|í)|añadir|elimin(a|á)|eliminar|' +
71
- 'modific(a|á)|modificar|reescrib(e|í)|reescribir|desplieg(a|á)|desplegar|' +
72
- 'hac(e|er|é)|resuelve|resolv(é|er)|soluciona|solucion(á|ar)|encárgate|encargate|' +
73
- 'ocúpate|ocupate|cambi(a|á)|cambiar|actualiz(a|á)|actualizar|' +
74
- 'implement|write|fix|build|refactor|migrate|add|remove|modify|update',
75
- );
76
-
77
- const READ_ONLY_VERBS = withUnicodeWordBoundary(
78
- 'investiga|investigar|busca|buscar|explora|explorar|lee|leer|explica|explicar|' +
79
- 'audita|auditar|analiza|analizar|compara|comparar|diagnostica|diagnosticar|' +
80
- 'revisa|revisar|averigua|averiguar|' +
81
- 'investigate|search|explore|read|explain|audit|analyze|compare|diagnose|review',
82
- );
83
-
84
- // A prompt-level signal that a whitelisted read-only subagent name should NOT be
85
- // trusted to exempt this call: real mutation risk in the text outranks a self-declared
86
- // label. Deliberately broad (over-includes) — a false positive here only means the
87
- // brief check still runs, which is cheap; a false negative would let a mutator hide.
88
- // Centralized in lib/signals.mjs (ES+EN) — see its header for the class this covers.
89
- const MUTATION_RISK_SIGNAL_PATTERN = MUTATION_RISK_SIGNAL;
90
-
91
- /** Evidence of a stated GOAL, captured so its trailing content can be measured. */
92
40
  const GOAL_PATTERN = withUnicodeWordBoundary(
93
41
  'objetivo|meta|el fin es|se busca|para lograr|para que|goal|objective',
94
42
  );
95
43
 
96
- /** Evidence of structured STEPS: a list/bullet/numbered form. Anchored per-line (`m`
97
- * flag) with a single bounded `\s*` after the line start, so there is no nested
98
- * quantifier for a backtracking engine to explode on. Global so every item's substance
99
- * can be checked, not just the first. */
44
+ // Anchored per line with a single bounded `\s*` so a backtracking engine has nothing to
45
+ // explode on; global so every item's substance can be checked.
100
46
  const STRUCTURED_STEPS_PATTERN = /^[ \t]*(?:[-*•]|\d+[.)])[ \t]+(\S.*)$/gm;
101
47
 
102
- /** Evidence of a DONE-WHEN / acceptance criterion. */
103
48
  const CRITERION_PATTERN = withUnicodeWordBoundary(
104
49
  'criterio|acceptance|asserts?|se considera (hecho|terminado|listo)|' +
105
50
  'debe (verificarse|cumplir|pasar)|hasta que|done.when|' +
@@ -107,9 +52,8 @@ const CRITERION_PATTERN = withUnicodeWordBoundary(
107
52
  'given.{0,20}when.{0,20}then',
108
53
  );
109
54
 
110
- // Filler words that do not count toward a section's substance even though they are
111
- // real words — otherwise "listo cuando funcione bien y quede resuelto satisfactoriamente
112
- // para todos" reads as substantial despite saying nothing concrete.
55
+ // Real words that say nothing concrete, so "listo cuando funcione bien y quede resuelto
56
+ // satisfactoriamente para todos" does not read as substance.
113
57
  const FILLER_WORDS = new Set(
114
58
  (
115
59
  'cosa cosas cualquier corresponda correspondiente relevante sistema bien listo ' +
@@ -120,70 +64,43 @@ const FILLER_WORDS = new Set(
120
64
  );
121
65
 
122
66
  function stripDiacritics(text) {
123
- return text.normalize('NFD').replace(/[̀-ͯ]/g, '');
67
+ return text.normalize('NFD').replace(/\p{M}/gu, '');
124
68
  }
125
69
 
126
- /** Non-filler word count in a chunk of text: what is left after dropping stopwords and
127
- * pure-filler vocabulary, so a marker followed only by empty phrasing does not count as
128
- * substance. */
129
70
  function substantiveWordCount(text) {
130
71
  const words = stripDiacritics(String(text).toLowerCase())
131
72
  .split(/[^\p{L}\p{N}]+/u)
132
73
  .filter(Boolean);
133
- return words.filter((word) => word.length > 2 && !FILLER_WORDS.has(word))
134
- .length;
74
+ return words.filter(
75
+ (word) => word.length >= MIN_WORD_LENGTH && !FILLER_WORDS.has(word),
76
+ ).length;
135
77
  }
136
78
 
137
- /** Text following a marker match, cut at end-of-line, so a following unrelated section
138
- * on the next line is not counted as this section's content. */
139
79
  function contentAfterMarker(prompt, markerMatch) {
140
80
  const from = markerMatch.index + markerMatch[0].length;
141
81
  const restOfLine = prompt.slice(from).split(/\r?\n/, 1)[0] ?? '';
142
82
  return restOfLine.replace(/^[\s:.\-–—]+/, '');
143
83
  }
144
84
 
145
- /** Whether the GOAL or CRITERION marker is followed by real substance: enough
146
- * substantive words, not just the marker itself or generic filler around it. */
147
- function markerHasSubstance(prompt, pattern) {
148
- const withGlobal = new RegExp(
149
- pattern.source,
150
- pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`,
151
- );
152
- const match = withGlobal.exec(prompt);
153
- if (!match) return false;
154
- const content = contentAfterMarker(prompt, match);
85
+ function hasSubstance(content) {
155
86
  return (
156
87
  content.trim().length >= MIN_SECTION_SUBSTANCE_LENGTH &&
157
- substantiveWordCount(content) >= 2
88
+ substantiveWordCount(content) >= MIN_SUBSTANTIVE_WORDS
158
89
  );
159
90
  }
160
91
 
161
- /** Whether at least one structured list item carries real substance (not just a single
162
- * filler word like "paso"). */
163
- function stepsHaveSubstance(prompt) {
164
- const matches = [...prompt.matchAll(STRUCTURED_STEPS_PATTERN)];
165
- return matches.some((match) => substantiveWordCount(match[1]) >= 2);
166
- }
167
-
168
- function isReadOnlySubagentName(toolInput, readOnlySubagents) {
169
- const type = String(
170
- toolInput.subagent_type ?? toolInput.subagentType ?? '',
171
- ).toLowerCase();
172
- return new Set(readOnlySubagents.map((name) => name.toLowerCase())).has(type);
173
- }
174
-
175
- /** A whitelisted subagent name exempts a call ONLY when the prompt carries no
176
- * mutation-risk signal. The name is a declared label, never a verified capability this
177
- * hook can check — documented here so a future reader does not mistake it for one — and
178
- * a real risk signal in the text must win over it. */
179
- function isReadOnlySubagent(toolInput, prompt, readOnlySubagents) {
180
- if (!isReadOnlySubagentName(toolInput, readOnlySubagents)) return false;
181
- return !MUTATION_RISK_SIGNAL_PATTERN.test(prompt);
92
+ function markerHasSubstance(prompt, pattern) {
93
+ const withGlobal = new RegExp(pattern.source, `${pattern.flags}g`);
94
+ for (const match of prompt.matchAll(withGlobal)) {
95
+ if (hasSubstance(contentAfterMarker(prompt, match))) return true;
96
+ }
97
+ return false;
182
98
  }
183
99
 
184
- function isReadOnlyRequest(prompt) {
185
- if (IMPLEMENTATION_VERBS.test(prompt)) return false;
186
- return READ_ONLY_VERBS.test(prompt);
100
+ function stepsHaveSubstance(prompt) {
101
+ return [...prompt.matchAll(STRUCTURED_STEPS_PATTERN)].some(
102
+ (match) => substantiveWordCount(match[1]) >= MIN_SUBSTANTIVE_WORDS,
103
+ );
187
104
  }
188
105
 
189
106
  function missingSignals(prompt) {
@@ -205,15 +122,13 @@ function missingSignals(prompt) {
205
122
  return missing;
206
123
  }
207
124
 
208
- function denyTooShort(prompt) {
209
- const excerpt = prompt.slice(0, PROMPT_EXCERPT_LENGTH);
210
- const ellipsis = prompt.length > PROMPT_EXCERPT_LENGTH ? '…' : '';
125
+ function denyTooShort(prompt, minBriefLength) {
211
126
  deny(
212
127
  CONFIG_KEY,
213
- `This delegation asks for implementation ("${excerpt}${ellipsis}") in a ${prompt.length}-character ` +
214
- 'prompt — too short to carry a goal, steps and a done-when criterion. State what this aims to ' +
215
- 'achieve, what concretely needs doing (as a list or steps), and how completion is verified, then ' +
216
- 'relaunch.',
128
+ `This delegation asks for implementation ("${promptExcerpt(prompt)}") in a ${prompt.length}-character ` +
129
+ `prompt — too short to carry a goal, steps and a done-when criterion (minBriefLength: ${minBriefLength}). ` +
130
+ 'State what this aims to achieve, what concretely needs doing (as a list or steps), and how ' +
131
+ 'completion is verified, then relaunch.',
217
132
  );
218
133
  }
219
134
 
@@ -239,6 +154,14 @@ function reportMissingSignals(missing) {
239
154
  );
240
155
  }
241
156
 
157
+ function isExempt(toolInput, prompt, readOnlySubagents) {
158
+ if (!prompt.trim()) return true;
159
+ if (isReadOnlySubagent(toolInput, prompt, readOnlySubagents)) return true;
160
+ if (isExemptQuery(prompt)) return true;
161
+ if (!isImplementationRequest(prompt)) return true;
162
+ return isDocumentaryRequest(prompt);
163
+ }
164
+
242
165
  runGate(
243
166
  {
244
167
  id: GATE_ID,
@@ -253,15 +176,11 @@ runGate(
253
176
  if (!toolInGroups(toolName, ['delegation'])) return;
254
177
 
255
178
  const prompt = delegationPromptOf(toolInput);
256
- if (!prompt.trim()) return;
257
- if (isReadOnlySubagent(toolInput, prompt, parameters.readOnlySubagents))
258
- return;
259
- if (isReadOnlyRequest(prompt)) return;
260
- if (!IMPLEMENTATION_VERBS.test(prompt)) return; // neither implementation nor read-only: do not guess
179
+ if (isExempt(toolInput, prompt, parameters.readOnlySubagents)) return;
261
180
 
262
181
  const trimmed = prompt.trim();
263
182
  if (trimmed.length < parameters.minBriefLength) {
264
- denyTooShort(trimmed);
183
+ denyTooShort(trimmed, parameters.minBriefLength);
265
184
  }
266
185
 
267
186
  reportMissingSignals(missingSignals(prompt));