@devrik-tools/claude-gates 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.es.md +129 -11
- package/README.md +120 -10
- package/cli/doctor.mjs +132 -0
- package/cli/evidence.mjs +78 -0
- package/cli/hooks-manifest.mjs +89 -0
- package/cli/index.mjs +124 -6
- package/cli/init.mjs +97 -3
- package/cli/log.mjs +70 -0
- package/cli/materialize.mjs +36 -2
- package/cli/registry.mjs +12 -1
- package/cli/selection.mjs +18 -0
- package/cli/smoke-fixtures.json +159 -25
- package/cli/task.mjs +104 -11
- package/cli/toggle.mjs +125 -0
- package/package.json +4 -3
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/doctor.mjs +47 -1
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
- package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +94 -215
- package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
- package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
- package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
- package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
- package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
- package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +53 -95
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
- package/plugins/gates/hooks/gates/library-docs/index.mjs +338 -0
- package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
- package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
- package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
- package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
- package/plugins/gates/hooks/gates/no-trivial-scripts/index.mjs +114 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
- package/plugins/gates/hooks/gates/require-monitor/index.mjs +126 -0
- package/plugins/gates/hooks/gates/require-task-split/index.mjs +88 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
- package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
- package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
- package/plugins/gates/hooks/hooks.json +152 -42
- package/plugins/gates/hooks/lib/config.mjs +6 -3
- package/plugins/gates/hooks/lib/delegation.mjs +352 -0
- package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
- package/plugins/gates/hooks/lib/git.mjs +262 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +451 -83
- package/plugins/gates/hooks/lib/session-state.mjs +150 -0
- package/plugins/gates/hooks/lib/signals.mjs +18 -13
- package/plugins/gates/hooks/lib/testing.mjs +148 -0
- package/plugins/gates/hooks/lib/tools.mjs +144 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/lib/task-store.mjs +12 -6
- package/plugins/tasks/hooks/register-requests.mjs +37 -10
- package/registry.json +249 -38
|
@@ -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 @
|
|
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.
|
|
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.
|
|
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
|
|
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 (
|
|
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.
|
|
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)
|
|
179
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|