@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
@@ -4,20 +4,20 @@
4
4
  "name": "claude-gates"
5
5
  },
6
6
  "metadata": {
7
- "description": "Installable deterministic gates (hooks) for Claude Code, selectable per project or globally with `npx @devrik99/claude-gates init`."
7
+ "description": "Installable deterministic gates (hooks) for Claude Code, selectable per project or globally with `npx @devrik-tools/claude-gates init`."
8
8
  },
9
9
  "plugins": [
10
10
  {
11
11
  "name": "gates",
12
12
  "source": "./plugins/gates",
13
13
  "description": "PreToolUse gates: destructive-command blocks, protected paths, delegation/spec/quality rules, tool-discovery and forge-pipeline enforcement. Which gates run is decided by .ai/config.json (project) or ~/.claude/claude-gates/config.json (global).",
14
- "version": "0.7.2"
14
+ "version": "0.9.0"
15
15
  },
16
16
  {
17
17
  "name": "tasks",
18
18
  "source": "./plugins/tasks",
19
19
  "description": "Deterministic task tracking: the model registers tasks via the CLI, a UserPromptSubmit hook reminds of open tasks every few messages, and a SessionStart hook lists active tasks when a session opens.",
20
- "version": "0.7.2"
20
+ "version": "0.9.0"
21
21
  }
22
22
  ]
23
23
  }
package/README.es.md CHANGED
@@ -30,7 +30,7 @@ npx @devrik-tools/claude-gates init
30
30
 
31
31
  Reinicia la sesión de Claude Code (o ejecuta `/plugin`) para que los hooks carguen.
32
32
 
33
- > **¿Por qué dos cosas?** El plugin **siempre trae los 41 gates**; la configuración decide
33
+ > **¿Por qué dos cosas?** El plugin **siempre trae los 49 gates**; la configuración decide
34
34
  > **cuáles se ejecutan**. Así puedes prender uno sin reinstalar: es una línea en un JSON.
35
35
 
36
36
  ---
@@ -67,7 +67,7 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
67
67
 
68
68
  ---
69
69
 
70
- ## Los gates (30, en 7 familias)
70
+ ## Los gates (49, en 11 familias)
71
71
 
72
72
  `[on]` = encendidos por defecto; `[off]` = los prendes si los quieres.
73
73
 
@@ -79,6 +79,7 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
79
79
  | `protected-paths` | on | Bloquea escrituras a `.env`, lockfiles y el propio harness. |
80
80
  | `root-whitelist` | on | Bloquea crear archivos/carpetas nuevos en la raíz fuera de la lista blanca. |
81
81
  | `no-blocking` | off | Bloquea `sleep`, `tail -f`, bucles de sondeo y servidores en primer plano. |
82
+ | `require-monitor` | on | Bloquea un comando en background (`run_in_background: true`) que no declara su monitor con el marcador `MONITOR-PLANNED:`, y bloquea cualquier ejecución posterior mientras quede un background sin monitorear. |
82
83
 
83
84
  ### 🤝 Delegation — exigencias sobre el brief al delegar a un subagente
84
85
  | Gate | | Qué hace |
@@ -88,6 +89,7 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
88
89
  | `risk-level` | off | Exige declarar el nivel (QUESTION/MICRO/STANDARD/HIGH-RISK). |
89
90
  | `circuit-breaker` | off | Corta la misma delegación reintentada sin cambios reales. |
90
91
  | `no-memory-dependency` | off | Bloquea un brief que depende de que el subagente "recuerde" la conversación (marcador `memory-not-needed` para un falso positivo). |
92
+ | `force-parallel` | on | Bloquea la enésima delegación secuencial consecutiva dentro de una ventana de tiempo: las delegaciones independientes se lanzan juntas en un solo mensaje (marcador `SEQUENTIAL-JUSTIFIED` cuando la segunda depende de verdad de la primera). |
91
93
 
92
94
  ### 📋 Spec-driven flow — solo aplican si el proyecto adoptó desarrollo por specs
93
95
  | Gate | | Qué hace |
@@ -117,6 +119,8 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
117
119
  | `atomic-commit` | off | Bloquea un `git commit` que no es atómico — que mezcla más de N naturalezas de cambio (código/tests/deps/config…) o stagea más archivos revisables de los que un commit debería llevar. Docs/imágenes/generados no cuentan. Marcador `[wip]` para un commit deliberadamente amplio. |
118
120
  | `no-coauthor` | on | Bloquea un `git commit` que lleve un trailer de atribución de IA (`Co-Authored-By`, `Generated with`, un trailer de sesión). Marcador `[allow-coauthor]` para un co-autor legítimo. |
119
121
  | `no-lint-suppression` | on | Bloquea una escritura que silencia el linter/type-checker (`eslint-disable`, `@ts-ignore`, una regla en `off`) en vez de arreglar el código. Marcador `lint-ok: <razón>` en la misma línea para un falso positivo documentado. |
122
+ | `no-explanatory-comments` | on | Bloquea una escritura de código que agrega comentarios que narran qué hace el código. Solo pasan comentarios de decisión (el porqué, un trade-off, una limitación), directivas de herramientas, `TODO`/`FIXME` y etiquetas JSDoc con tipo. Juzga solo comentarios nuevos (diff contra disco). `comment-ok: <razón>` para una excepción documentada. |
123
+ | `no-trivial-scripts` | on | Bloquea un script inline de intérprete que hace una operación de archivos que las herramientas `Edit`/`Write` resuelven directo (`node -e` con `writeFileSync`, `python -c` con `open(…, 'w')`, `sed -i`, `perl -i`, `Set-Content`/`Add-Content`). Los scripts inline que solo computan no caen. |
120
124
 
121
125
  ### 🔎 Tool discovery — no reinventar la rueda
122
126
  | Gate | | Qué hace |
@@ -124,11 +128,38 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
124
128
  | `reuse-before-build` | off | Antes de construir una herramienta, consulta el mapa de herramientas del proyecto; bloquea si no auditaste (local → Context7 → web). |
125
129
  | `tool-map` | off | Registra las herramientas descubiertas en `.ai/tool-map.json` para no volver a explorar. |
126
130
 
131
+ ### 🧠 Research flow — la memoria primero, nunca adivinar una librería
132
+
133
+ | Gate | | Qué hace |
134
+ | -------------- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
135
+ | `engram-first` | on | Bloquea `WebSearch`, `WebFetch` y context7 hasta que en la sesión haya corrido un `mem_search` ([engram](https://github.com/Gentleman-Programming/engram) es la primera fuente); bloquea el Stop mientras hubo investigación sin un `mem_save` posterior; al arrancar avisa una vez si engram cloud está configurado y el proyecto no está enrolado. |
136
+ | `library-docs` | on | Bloquea una escritura que importa un paquete que el proyecto no usa en ningún lado, salvo que en esta sesión se haya consultado: un hit de engram sobre él, o docs de context7 seguidas de un `mem_save`. Nunca adivinar la API de una librería. |
137
+
127
138
  ### 🏭 Forge pipeline — obliga a seguir el flujo de forge
128
139
  | Gate | | Qué hace |
129
140
  |---|---|---|
130
141
  | `forge-flow` | off | En un proyecto que adoptó [forge](https://github.com/DevRik99/forge-mcp), bloquea editar/ejecutar si no hay un run de forge activo. Cierra el hueco que el MCP no puede: te obliga a pasar por el pipeline. |
131
142
 
143
+ ### 🤖 Autonomy — que una corrida sin supervisión decida en vez de preguntar
144
+
145
+ | Gate | | Qué hace |
146
+ |---|---|---|
147
+ | `autonomous-mode` | off | Con el modo autónomo prendido, bloquea `AskUserQuestion` y bloquea que el turno termine solo para esperar: reinyecta, una vez por ciclo, la instrucción de decidir y seguir, dejando pendiente solo lo que de verdad necesita al usuario. |
148
+
149
+ ### ⏹️ Completion — el turno no termina con trabajo abierto
150
+
151
+ | Gate | | Qué hace |
152
+ |---|---|---|
153
+ | `stop-pending` | on | Bloquea el evento Stop mientras el proyecto tiene tareas activas (`open`/`in_forge`); las lista y explica cómo cerrarlas con evidencia verificada o abandonarlas. Las tareas `blocked` no retienen el turno por defecto. |
154
+ | `require-task-split` | on | Bloquea escrituras y ejecución mientras una tarea activa más grande que `small` no tenga sub-tareas registradas: primero se parte en piezas verificables por separado (`task add --parent <id>`). |
155
+
156
+ ### 🗂️ Task tracking — el plugin de tareas
157
+
158
+ | Gate | | Qué hace |
159
+ |---|---|---|
160
+ | `remind-open-tasks` | on | Hace que el asistente clasifique y registre el trabajo nuevo por el CLI, y recita las tareas activas cada N mensajes. |
161
+ | `list-tasks-on-session-start` | on | Lista las tareas activas del proyecto al abrir una sesión. Silencioso si no hay ninguna. |
162
+
132
163
  ### 🩺 Sesión y contexto — validaciones al arrancar e inyección de capacidades
133
164
  | Gate | | Qué hace |
134
165
  |---|---|---|
@@ -152,7 +183,7 @@ ves y editas cada perilla:
152
183
  ```json
153
184
  {
154
185
  "adopted": "partial",
155
- "gateVersion": "3.0.0",
186
+ "gateVersion": "3.1.0",
156
187
  "gates": {
157
188
  "blockDestructiveShellCommands": {
158
189
  "enabled": true,
@@ -164,7 +195,17 @@ ves y editas cada perilla:
164
195
  }
165
196
  ```
166
197
 
167
- - **Apagar un gate:** `"enabled": false`. Se apaga al instante, sin reinstalar.
198
+ - **Apagar un gate:** `"enabled": false`. Se apaga al instante, sin reinstalar. O desde el CLI:
199
+ `claude-gates disable <gate>` / `claude-gates enable <gate>` (ids de gate, claves de
200
+ configuración, ids de familia o `all`; `--project` o `--global`), y `claude-gates status`
201
+ para ver qué está prendido en el directorio actual y de dónde sale cada valor.
202
+ - **Volver a correr `init` mergea, nunca pisa.** Un gate que ya está en el archivo solo cambia
203
+ si lo nombras (`--gates`, `--families`, `--all`, `--none`) y, cuando su `enabled` cambiaría,
204
+ lo confirmas gate por gate. Con `--yes` (sin TTY) se conservan los valores existentes y se
205
+ reportan; con `--force` se aplican sin preguntar.
206
+ - **Un parámetro con el tipo equivocado nunca rompe un gate.** Un string donde se espera una
207
+ lista, o un regex mal escrito, cae al default incorporado y se reporta una vez por sesión
208
+ en el propio mensaje del gate (y en el log de decisiones).
168
209
  - **Ajustar su comportamiento:** editas sus parámetros (la lista blanca, los patrones, los
169
210
  umbrales). Lo que declara el proyecto **reemplaza** el default del gate.
170
211
  - Un gate que no aparece en la configuración usa su default del catálogo. Las claves que ya
@@ -175,8 +216,10 @@ ves y editas cada perilla:
175
216
  (una frase que no depende de memoria), `[allow-coauthor]` (un co-autor legítimo en un
176
217
  commit), `lint-ok: <razón>` (un falso positivo documentado del linter), `[skip-lint]`
177
218
  (saltea el chequeo de staged-lint por un commit), `[wip]` (permite un commit
178
- deliberadamente amplio, no atómico). `dependency-skills` se exime vía su lista
179
- `depsWithoutOwnApi`.
219
+ deliberadamente amplio, no atómico), `comment-ok: <razón>` (un comentario explicativo que
220
+ debe quedarse), `SEQUENTIAL-JUSTIFIED` (una delegación que sí depende de la anterior),
221
+ `MONITOR-PLANNED:` (el comando en background declara cómo se va a monitorear).
222
+ `dependency-skills` se exime vía su lista `depsWithoutOwnApi`.
180
223
  - **Inyección de capacidades:** `capability-map` (on por defecto) es totalmente ajustable —
181
224
  elegí qué tipos exponer (`"kinds": ["skills", "agents", "commands"]`), limitá cada blurb
182
225
  (`maxClauseChars`, default 120), agregá raíces extra por tipo, regulá cada cuánto se
@@ -197,6 +240,52 @@ ves y editas cada perilla:
197
240
 
198
241
  ---
199
242
 
243
+ ## Log de decisiones
244
+
245
+ Cada deny, warn y bloqueo de Stop se agrega como una línea JSON a
246
+ `<raíz>/.ai/gates-log.jsonl` (fecha, gate, clave de configuración, herramienta, un resumen de
247
+ una línea de la acción, el motivo, la sesión). Se lee con:
248
+
249
+ ```bash
250
+ claude-gates log # últimas 30 decisiones de este proyecto
251
+ claude-gates log --deny --gate bash-commands --tail 100
252
+ claude-gates log --since 2026-09-01T00:00:00Z --json
253
+ ```
254
+
255
+ El archivo rota una vez a los 5 MB (`gates-log.1.jsonl`). `CLAUDE_GATES_LOG=0` lo desactiva.
256
+
257
+ ---
258
+
259
+ ## Tareas: se registran con criterio y se cierran con evidencia verificada
260
+
261
+ Una tarea lleva su criterio de verificación **desde que se crea** — `task add` rechaza una
262
+ tarea que nadie puede probar terminada — y `task close` rechaza texto libre: está hecha solo
263
+ cuando la verificación pasa de verdad.
264
+
265
+ ```bash
266
+ # Registrar: el criterio es obligatorio (--verify-command o --verify-path)
267
+ claude-gates task add "migrar el cargador de configuración" \
268
+ --size medium --verify-command "npm test" --verify-expect "fail 0"
269
+ claude-gates task add "escribir la guía de migración" \
270
+ --parent <id> --verify-path docs/migration.md --verify-contains "## Upgrading"
271
+
272
+ claude-gates task list [--all] # tareas activas (o todo el historial)
273
+
274
+ # Cerrar: sin --check/--exists, se vuelve a correr el criterio propio de la tarea
275
+ claude-gates task close <id>
276
+ claude-gates task close <id> --check "npm test" --expect "fail 0" --note "suite en verde"
277
+ claude-gates task close <id> --exists dist/report.html --contains "All green"
278
+ claude-gates task abandon <id> --reason "obsoleta"
279
+ claude-gates task promote <id> <runId> # vincula la tarea a un run de forge
280
+ ```
281
+
282
+ El resultado verificado (comando, código de salida, cola de la salida, fecha) queda guardado
283
+ con la tarea. Dos gates se apoyan en este store: `require-task-split` bloquea implementar una
284
+ tarea más grande que `small` sin sub-tareas, y `stop-pending` impide que el turno termine
285
+ mientras queden tareas abiertas.
286
+
287
+ ---
288
+
200
289
  ## Comandos del CLI
201
290
 
202
291
  ```bash
@@ -206,12 +295,37 @@ npx @devrik-tools/claude-gates init
206
295
  # Sin menú (para CI o scripts):
207
296
  claude-gates init --project|--global --defaults|--all|--none|--families a,b|--gates x,y --yes --dry-run
208
297
  claude-gates init --no-install # escribe la configuración pero no instala el plugin
298
+ claude-gates init --force # aplica cambios a gates ya presentes sin preguntar
299
+
300
+ # Prender, apagar e inspeccionar lo que corre aquí:
301
+ claude-gates enable <gate|familia|all> [--project|--global]
302
+ claude-gates disable <gate|familia|all> [--project|--global]
303
+ claude-gates status # on/off efectivo por gate y su origen (project/global/default)
304
+ claude-gates log [--tail N] [--deny] [--gate id] [--since iso] [--json]
305
+ claude-gates doctor # ¿Claude Code corre ESTA versión del paquete?
306
+
307
+ # Tareas (el store que leen los gates de completion):
308
+ claude-gates task add <título> --size <tamaño> --verify-command <cmd>|--verify-path <ruta> [--parent <id>]
309
+ claude-gates task list [--all]
310
+ claude-gates task close <id> [--check <cmd> --expect <texto>] [--exists <ruta> --contains <texto>]
311
+ claude-gates task abandon <id> --reason <texto>
312
+ claude-gates task promote <id> <runId>
209
313
 
210
314
  # Inspeccionar el catálogo:
211
315
  claude-gates registry --list # lista familias y gates
212
- claude-gates registry --check # valida registry.json
316
+ claude-gates registry --check # valida registry.json y que hooks.json esté sincronizado
317
+ claude-gates registry --sync-hooks # regenera el hooks.json de cada plugin desde el registry
318
+
319
+ # Verificar que los gates realmente reaccionan (no solo que están enganchados):
320
+ claude-gates smoke # le da a cada gate una violación conocida; sale distinto de 0 si alguno no bloquea/avisa
213
321
  ```
214
322
 
323
+ `smoke` es el chequeo de comportamiento que `registry --check` (estructura) y el hook doctor
324
+ (que los archivos existan) no hacen: le da a cada gate una violación conocida y confirma que
325
+ de verdad deniega o avisa. Los gates cuya violación necesita estado sembrado (una db, un repo
326
+ git, estado entre llamadas) reportan `skip`, nunca un falso pase. Se corre después de
327
+ instalar, o en CI, para detectar un gate enganchado que en silencio deja pasar todo.
328
+
215
329
  ---
216
330
 
217
331
  ## Estructura del repositorio
@@ -225,15 +339,18 @@ cli/ El CLI de npm (commander + @clack/prompts + zo
225
339
  init.mjs · install.mjs Flujo interactivo + instalar el plugin.
226
340
  plugins/gates/ El plugin de gates.
227
341
  .claude-plugin/plugin.json
228
- hooks/hooks.json Una entrada por gate (matcher + comando). Lo carga Claude Code.
342
+ hooks/hooks.json Generado desde registry.json (`registry --sync-hooks`). Lo carga Claude Code.
229
343
  hooks/lib/ Código compartido de los hooks (Node built-ins only).
230
344
  hooks/gates/<id>/ Un gate por carpeta: index.mjs (la regla) + test.mjs (su test).
231
- plugins/tasks/ El plugin de tareas (en construcción): persiste tareas por proyecto.
345
+ plugins/tasks/ El plugin de tareas: persiste tareas por proyecto, recuerda las abiertas
346
+ y las lista al arrancar la sesión.
232
347
  .claude-plugin/marketplace.json Lista los plugins del marketplace.
233
348
  ```
234
349
 
235
350
  **Agregar un gate** = una carpeta en `plugins/gates/hooks/gates/<id>/` (con `index.mjs` y
236
- `test.mjs`) + una entrada en `registry.json`. El resto se deriva solo.
351
+ `test.mjs`) + una entrada en `registry.json`, y luego `claude-gates registry --sync-hooks`.
352
+ Los gates comparten `hooks/lib/` (lectura del payload, normalización de git, estado por
353
+ sesión, vocabulario de delegación, el log de decisiones, el harness de tests): un gate es solo su regla.
237
354
 
238
355
  ---
239
356
 
@@ -245,7 +362,8 @@ npm run registry:check # valida el catálogo
245
362
  npm run lint # eslint (boundaries, no-magic-numbers, sonarjs, cspell…)
246
363
  ```
247
364
 
248
- Cada gate se testea aislado: `node --test plugins/gates/hooks/gates/<id>/test.mjs`.
365
+ Cada gate se testea aislado: `node --test plugins/gates/hooks/gates/<id>/test.mjs`
366
+ (nombra los archivos de test: un glob que también matchee `index.mjs` se cuelga, porque un gate espera stdin).
249
367
 
250
368
  > Este repositorio trae su propio `.ai/config.json` que apaga localmente los gates que
251
369
  > darían falso positivo al **editar los gates mismos** (por ejemplo, `audit-before-build`
package/README.md CHANGED
@@ -29,7 +29,7 @@ npx @devrik-tools/claude-gates init
29
29
 
30
30
  Restart the Claude Code session (or run `/plugin`) so the hooks load.
31
31
 
32
- > **Why two things?** The plugin **always ships all 41 gates**; the config decides **which
32
+ > **Why two things?** The plugin **always ships all 49 gates**; the config decides **which
33
33
  > ones run**. So you can turn one on without reinstalling — it is one line in a JSON file.
34
34
 
35
35
  ---
@@ -65,7 +65,7 @@ works even if you install one on its own.
65
65
 
66
66
  ---
67
67
 
68
- ## The gates (in families)
68
+ ## The gates (49, in 11 families)
69
69
 
70
70
  `[on]` = enabled by default; `[off]` = enable it if you want it.
71
71
 
@@ -78,6 +78,7 @@ works even if you install one on its own.
78
78
  | `protected-paths` | on | Blocks writes to `.env`, lockfiles and the harness itself. |
79
79
  | `root-whitelist` | on | Blocks new root-level files/folders outside a whitelist. |
80
80
  | `no-blocking` | off | Blocks `sleep`, `tail -f`, polling loops and foreground servers. |
81
+ | `require-monitor` | on | Blocks a background command (`run_in_background: true`) that does not declare its monitor with a `MONITOR-PLANNED:` marker, and blocks further execution while an unmonitored background command is still pending. |
81
82
 
82
83
  ### 🤝 Delegation — requirements on the brief when delegating to a subagent
83
84
 
@@ -88,6 +89,7 @@ works even if you install one on its own.
88
89
  | `risk-level` | off | Requires a declared level (QUESTION/MICRO/STANDARD/HIGH-RISK). |
89
90
  | `circuit-breaker` | off | Cuts the same delegation retried without real changes. |
90
91
  | `no-memory-dependency` | off | Blocks a brief that relies on the subagent "remembering" the chat (add `memory-not-needed` to allow a false positive). |
92
+ | `force-parallel` | on | Blocks the Nth consecutive sequential delegation inside a time window: independent delegations must be launched together in one message (add `SEQUENTIAL-JUSTIFIED` when the second one really depends on the first). |
91
93
 
92
94
  ### 📋 Spec-driven flow — only relevant if the project adopted spec-driven development
93
95
 
@@ -119,6 +121,8 @@ works even if you install one on its own.
119
121
  | `atomic-commit` | off | Blocks a `git commit` that is not atomic — one mixing more than N natures of change (code/tests/deps/config…) or staging more reviewable files than a commit should carry. Docs/assets/generated aren't counted. Add `[wip]` for a deliberately broad commit. |
120
122
  | `no-coauthor` | on | Blocks a `git commit` carrying an AI/agent attribution trailer (`Co-Authored-By`, `Generated with`, a session trailer). Add `[allow-coauthor]` for one legitimate co-author. |
121
123
  | `no-lint-suppression` | on | Blocks a write that silences the linter/type-checker (`eslint-disable`, `@ts-ignore`, a rule set to `off`) instead of fixing the code. Add `lint-ok: <reason>` on the same line for a documented false positive. |
124
+ | `no-explanatory-comments` | on | Blocks a code write that adds comments narrating what the code does. Only decision comments (why, trade-off, limitation), tool directives, `TODO`/`FIXME` and typed JSDoc tags pass. Judges only new comments (diff against disk). Add `comment-ok: <reason>` for a documented exception. |
125
+ | `no-trivial-scripts` | on | Blocks an inline interpreter script that does a file operation the `Edit`/`Write` tool handles directly (`node -e` with `writeFileSync`, `python -c` with `open(…, 'w')`, `sed -i`, `perl -i`, `Set-Content`/`Add-Content`). Inline scripts that only compute are not caught. |
122
126
 
123
127
  ### 🔎 Tool discovery — don't reinvent the wheel
124
128
 
@@ -127,12 +131,39 @@ works even if you install one on its own.
127
131
  | `reuse-before-build` | off | Before building a tool, consults the project tool map; blocks if you did not audit (local → Context7 → web). |
128
132
  | `tool-map` | off | Records discovered tools in `.ai/tool-map.json` so exploration is not repeated. |
129
133
 
134
+ ### 🧠 Research flow — memory first, never guess a library
135
+
136
+ | Gate | | What it does |
137
+ | -------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
+ | `engram-first` | on | Blocks `WebSearch`, `WebFetch` and context7 until a `mem_search` ran in the session ([engram](https://github.com/Gentleman-Programming/engram) is the first source); blocks the Stop event while research happened with no later `mem_save`; at session start warns once when engram cloud is configured but the project is not enrolled. |
139
+ | `library-docs` | on | Blocks a write that imports a package the project does not use anywhere yet unless this session looked it up: an engram hit about it, or context7 docs followed by a `mem_save`. Never guess a library API. |
140
+
130
141
  ### 🏭 Forge pipeline — enforces the forge workflow
131
142
 
132
143
  | Gate | | What it does |
133
144
  | ------------ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
145
  | `forge-flow` | off | In a project that adopted [forge](https://github.com/DevRik99/forge-mcp), blocks editing/running unless an active forge run exists. Closes the hole the MCP cannot: it forces you through the pipeline. |
135
146
 
147
+ ### 🤖 Autonomy — let an unattended run decide instead of asking
148
+
149
+ | Gate | | What it does |
150
+ | ------------------ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
151
+ | `autonomous-mode` | off | With autonomous mode on, blocks `AskUserQuestion` and blocks a turn that ends only to wait: it re-injects, once per cycle, the instruction to decide and proceed, leaving pending only what genuinely needs the user. |
152
+
153
+ ### ⏹️ Completion — a turn does not end with work still open
154
+
155
+ | Gate | | What it does |
156
+ | -------------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
157
+ | `stop-pending` | on | Blocks the Stop event while this project has active tasks (`open`/`in_forge`); lists them and how to close them with verified evidence or abandon them. `blocked` tasks don't hold the turn by default. |
158
+ | `require-task-split` | on | Blocks writes and execution while an active task larger than `small` has no sub-tasks registered: split it into independently verifiable pieces first (`task add --parent <id>`). |
159
+
160
+ ### 🗂️ Task tracking — the tasks plugin
161
+
162
+ | Gate | | What it does |
163
+ | ----------------------------- | --- | ---------------------------------------------------------------------------------------------------------- |
164
+ | `remind-open-tasks` | on | Makes the assistant classify and register new work via the CLI, and recites the active tasks every N messages. |
165
+ | `list-tasks-on-session-start` | on | Lists this project's active tasks when a session opens. Silent when there are none. |
166
+
136
167
  ### 🩺 Session & context — startup checks and capability injection
137
168
 
138
169
  | Gate | | What it does |
@@ -157,7 +188,7 @@ see and edit every knob:
157
188
  ```json
158
189
  {
159
190
  "adopted": "partial",
160
- "gateVersion": "3.0.0",
191
+ "gateVersion": "3.1.0",
161
192
  "gates": {
162
193
  "blockDestructiveShellCommands": {
163
194
  "enabled": true,
@@ -175,7 +206,17 @@ see and edit every knob:
175
206
  }
176
207
  ```
177
208
 
178
- - **Turn a gate off:** `"enabled": false`. Off instantly, no reinstall.
209
+ - **Turn a gate off:** `"enabled": false`. Off instantly, no reinstall. Or from the CLI:
210
+ `claude-gates disable <gate>` / `claude-gates enable <gate>` (gate ids, config keys,
211
+ family ids or `all`; `--project` or `--global`), and `claude-gates status` to see what is
212
+ on for the current directory and where each value comes from.
213
+ - **Re-running `init` merges, never overwrites.** A gate already in the file only changes
214
+ when you name it (`--gates`, `--families`, `--all`, `--none`) and, when its `enabled`
215
+ would flip, you confirm it gate by gate. With `--yes` (no TTY) the existing values are
216
+ kept and reported; pass `--force` to apply them without asking.
217
+ - **A wrong-typed param never breaks a gate.** A string where a list is expected, or a
218
+ malformed regex, falls back to the built-in default and is reported once per session in
219
+ the gate's own message (and in the decision log).
179
220
  - **Tune its behavior:** edit its parameters (the whitelist, the patterns, the thresholds).
180
221
  What the project declares **replaces** the gate's default.
181
222
  - A gate absent from the config uses its catalog default. Keys you already had in the file
@@ -188,7 +229,10 @@ see and edit every knob:
188
229
  non-memory phrase), `[allow-coauthor]` (one legitimate co-author on a commit),
189
230
  `lint-ok: <reason>` (a documented linter false positive), `[skip-lint]` (skip the
190
231
  staged-lint check for one commit), `[wip]` (allow one deliberately broad,
191
- non-atomic commit). `dependency-skills` opts out via its `depsWithoutOwnApi` list.
232
+ non-atomic commit), `comment-ok: <reason>` (one explanatory comment that must stay),
233
+ `SEQUENTIAL-JUSTIFIED` (a delegation that genuinely depends on the previous one),
234
+ `MONITOR-PLANNED:` (the background command declares how it will be monitored).
235
+ `dependency-skills` opts out via its `depsWithoutOwnApi` list.
192
236
  - **Capability injection:** `capability-map` (on by default) is fully tunable — pick which
193
237
  kinds to surface (`"kinds": ["skills", "agents", "commands"]`), cap each blurb
194
238
  (`maxClauseChars`, default 120), add extra roots per kind, throttle how often the full
@@ -208,6 +252,52 @@ see and edit every knob:
208
252
 
209
253
  ---
210
254
 
255
+ ## Decision log
256
+
257
+ Every deny, warn and Stop-block is appended as one JSON line to
258
+ `<root>/.ai/gates-log.jsonl` (timestamp, gate, config key, tool, a one-line summary of the
259
+ action, the reason, the session). Read it with:
260
+
261
+ ```bash
262
+ claude-gates log # last 30 decisions for this project
263
+ claude-gates log --deny --gate bash-commands --tail 100
264
+ claude-gates log --since 2026-09-01T00:00:00Z --json
265
+ ```
266
+
267
+ The file rotates once at 5 MB (`gates-log.1.jsonl`). Set `CLAUDE_GATES_LOG=0` to disable it.
268
+
269
+ ---
270
+
271
+ ## Tasks: registered with a criterion, closed with verified evidence
272
+
273
+ A task carries its verification criterion **from the moment it is created** — `task add`
274
+ refuses a task nobody can prove done — and `task close` refuses free text: it is done only
275
+ when the check actually passes.
276
+
277
+ ```bash
278
+ # Register: the criterion is mandatory (--verify-command or --verify-path)
279
+ claude-gates task add "migrate the config loader" \
280
+ --size medium --verify-command "npm test" --verify-expect "fail 0"
281
+ claude-gates task add "write the migration guide" \
282
+ --parent <id> --verify-path docs/migration.md --verify-contains "## Upgrading"
283
+
284
+ claude-gates task list [--all] # active tasks (or the whole history)
285
+
286
+ # Close: with no --check/--exists, the task's own verify criterion is re-run
287
+ claude-gates task close <id>
288
+ claude-gates task close <id> --check "npm test" --expect "fail 0" --note "suite green"
289
+ claude-gates task close <id> --exists dist/report.html --contains "All green"
290
+ claude-gates task abandon <id> --reason "obsolete"
291
+ claude-gates task promote <id> <runId> # link the task to a forge run
292
+ ```
293
+
294
+ The verified result (command, exit code, output tail, timestamp) is stored with the task.
295
+ Two gates lean on this store: `require-task-split` blocks implementing a task bigger than
296
+ `small` that has no sub-tasks, and `stop-pending` blocks the turn from ending while tasks
297
+ are still open.
298
+
299
+ ---
300
+
211
301
  ## CLI commands
212
302
 
213
303
  ```bash
@@ -217,10 +307,26 @@ npx @devrik-tools/claude-gates init
217
307
  # Non-interactive (for CI or scripts):
218
308
  claude-gates init --project|--global --defaults|--all|--none|--families a,b|--gates x,y --yes --dry-run
219
309
  claude-gates init --no-install # write the config but do not install the plugin
310
+ claude-gates init --force # apply changes to gates already in the file without asking
311
+
312
+ # Toggle and inspect what runs here:
313
+ claude-gates enable <gate|family|all> [--project|--global]
314
+ claude-gates disable <gate|family|all> [--project|--global]
315
+ claude-gates status # effective on/off per gate and its source (project/global/default)
316
+ claude-gates log [--tail N] [--deny] [--gate id] [--since iso] [--json]
317
+ claude-gates doctor # is Claude Code running THIS package version of the plugins?
318
+
319
+ # Tasks (the store the completion gates read):
320
+ claude-gates task add <title> --size <size> --verify-command <cmd>|--verify-path <path> [--parent <id>]
321
+ claude-gates task list [--all]
322
+ claude-gates task close <id> [--check <cmd> --expect <text>] [--exists <path> --contains <text>]
323
+ claude-gates task abandon <id> --reason <text>
324
+ claude-gates task promote <id> <runId>
220
325
 
221
326
  # Inspect the catalog:
222
327
  claude-gates registry --list # list families and gates
223
- claude-gates registry --check # validate registry.json
328
+ claude-gates registry --check # validate registry.json and that hooks.json is in sync
329
+ claude-gates registry --sync-hooks # regenerate each plugin's hooks.json from the registry
224
330
 
225
331
  # Verify the gates actually react (not just that they are wired):
226
332
  claude-gates smoke # feed each gate a known violation; exits non-zero if any does not block/warn
@@ -245,15 +351,18 @@ cli/ The npm CLI (commander + @clack/prompts + zod)
245
351
  init.mjs · install.mjs Interactive flow + install the plugin.
246
352
  plugins/gates/ The gates plugin.
247
353
  .claude-plugin/plugin.json
248
- hooks/hooks.json One entry per gate (matcher + command). Loaded by Claude Code.
354
+ hooks/hooks.json Generated from registry.json (`registry --sync-hooks`). Loaded by Claude Code.
249
355
  hooks/lib/ Shared hook code (Node built-ins only).
250
356
  hooks/gates/<id>/ One gate per folder: index.mjs (the rule) + test.mjs (its test).
251
- plugins/tasks/ The tasks plugin (work in progress): persists per-project tasks.
357
+ plugins/tasks/ The tasks plugin: persists per-project tasks, reminds of the open ones
358
+ and lists them on session start.
252
359
  .claude-plugin/marketplace.json Lists the marketplace plugins.
253
360
  ```
254
361
 
255
362
  **Adding a gate** = one folder in `plugins/gates/hooks/gates/<id>/` (with `index.mjs` and
256
- `test.mjs`) + one entry in `registry.json`. Everything else derives automatically.
363
+ `test.mjs`) + one entry in `registry.json`, then `claude-gates registry --sync-hooks`.
364
+ Gates share `hooks/lib/` (payload readers, git normalization, session state, delegation
365
+ vocabulary, the decision log, the test harness): a gate file is only its rule.
257
366
 
258
367
  ---
259
368
 
@@ -265,7 +374,8 @@ npm run registry:check # validate the catalog
265
374
  npm run lint # eslint (boundaries, no-magic-numbers, sonarjs, cspell…)
266
375
  ```
267
376
 
268
- Each gate is tested in isolation: `node --test plugins/gates/hooks/gates/<id>/test.mjs`.
377
+ Each gate is tested in isolation: `node --test plugins/gates/hooks/gates/<id>/test.mjs`
378
+ (name the test files: a glob that also matches `index.mjs` hangs, because a gate waits on stdin).
269
379
 
270
380
  > This repo ships its own `.ai/config.json` that locally disables the gates that would
271
381
  > false-positive when **editing the gates themselves** (e.g. `audit-before-build` thinks
package/cli/doctor.mjs ADDED
@@ -0,0 +1,132 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { REPOSITORY_ROOT } from './constants.mjs';
5
+
6
+ const CLAUDE_BIN = 'claude';
7
+
8
+ function runClaudeDefault(commandArguments) {
9
+ return execFileSync(CLAUDE_BIN, commandArguments, {
10
+ encoding: 'utf8',
11
+ stdio: 'pipe',
12
+ });
13
+ }
14
+
15
+ export function parsePluginList(listing) {
16
+ const plugins = [];
17
+ let current = null;
18
+ for (const line of listing.split(/\r?\n/)) {
19
+ const name = /^\s*(?:❯\s*)?([\w-]+)@([\w-]+)\s*$/.exec(line);
20
+ if (name) {
21
+ current = { plugin: name[1], marketplace: name[2] };
22
+ plugins.push(current);
23
+ continue;
24
+ }
25
+ if (!current) continue;
26
+ const version = /^\s*Version:\s*(\S+)/.exec(line);
27
+ if (version) current.version = version[1];
28
+ const scope = /^\s*Scope:\s*(\S+)/.exec(line);
29
+ if (scope) current.scope = scope[1];
30
+ }
31
+ return plugins;
32
+ }
33
+
34
+ export function parseMarketplaceSources(listing) {
35
+ const sources = {};
36
+ let current = null;
37
+ for (const line of listing.split(/\r?\n/)) {
38
+ const name = /^\s*(?:❯\s*)?([\w-]+)\s*$/.exec(line);
39
+ if (name) {
40
+ current = name[1];
41
+ continue;
42
+ }
43
+ const source = /^\s*Source:\s*(\w+)\s*\(([^)]+)\)/.exec(line);
44
+ if (source && current)
45
+ sources[current] = { kind: source[1], path: source[2] };
46
+ }
47
+ return sources;
48
+ }
49
+
50
+ function withoutTrailingSlashes(text) {
51
+ let end = text.length;
52
+ while (end > 0 && (text[end - 1] === '/' || text[end - 1] === '\\')) end -= 1;
53
+ return text.slice(0, end).toLowerCase();
54
+ }
55
+
56
+ function compareVersions(a, b) {
57
+ const left = String(a).split('.').map(Number);
58
+ const right = String(b).split('.').map(Number);
59
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
60
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
61
+ if (difference !== 0) return difference;
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ export function diagnose({ runClaude = runClaudeDefault } = {}) {
67
+ const manifest = JSON.parse(
68
+ readFileSync(
69
+ join(REPOSITORY_ROOT, '.claude-plugin', 'marketplace.json'),
70
+ 'utf8',
71
+ ),
72
+ );
73
+ const packageVersion = JSON.parse(
74
+ readFileSync(join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
75
+ ).version;
76
+ const problems = [];
77
+ const facts = [`package: ${packageVersion} at ${REPOSITORY_ROOT}`];
78
+ let plugins;
79
+ let sources;
80
+ try {
81
+ plugins = parsePluginList(runClaude(['plugin', 'list']));
82
+ sources = parseMarketplaceSources(
83
+ runClaude(['plugin', 'marketplace', 'list']),
84
+ );
85
+ } catch (error) {
86
+ problems.push(
87
+ `could not query Claude Code (${error?.message ?? error}); is \`claude\` on PATH?`,
88
+ );
89
+ return { packageVersion, facts, problems };
90
+ }
91
+ const ours = plugins.filter((plugin) =>
92
+ manifest.plugins.some((declared) => declared.name === plugin.plugin),
93
+ );
94
+ if (ours.length === 0) {
95
+ problems.push(
96
+ 'no claude-gates plugin is installed in Claude Code: run `claude-gates init --global --defaults --yes`',
97
+ );
98
+ }
99
+ for (const plugin of ours) {
100
+ facts.push(
101
+ `${plugin.plugin}@${plugin.marketplace} ${plugin.version} (${plugin.scope})`,
102
+ );
103
+ if (compareVersions(plugin.version, packageVersion) < 0) {
104
+ problems.push(
105
+ `${plugin.plugin} (${plugin.scope}) runs ${plugin.version} but this package is ${packageVersion}: ` +
106
+ 'run `claude-gates init --global --defaults --yes` from this package to re-point the marketplace and reinstall',
107
+ );
108
+ }
109
+ const source = sources[plugin.marketplace];
110
+ if (source?.kind === 'Directory') {
111
+ const same =
112
+ withoutTrailingSlashes(source.path) ===
113
+ withoutTrailingSlashes(REPOSITORY_ROOT);
114
+ if (!same)
115
+ problems.push(
116
+ `marketplace "${plugin.marketplace}" serves from ${source.path}, not from this package (${REPOSITORY_ROOT})`,
117
+ );
118
+ }
119
+ }
120
+ return { packageVersion, facts, problems };
121
+ }
122
+
123
+ export function renderDiagnosis(report) {
124
+ const lines = report.facts.map((fact) => ` ${fact}`);
125
+ if (report.problems.length === 0) lines.push('\nAll good.');
126
+ else
127
+ lines.push(
128
+ '\nProblems:',
129
+ ...report.problems.map((problem) => ` - ${problem}`),
130
+ );
131
+ return `${lines.join('\n')}\n`;
132
+ }