@devrik-tools/claude-gates 0.8.0 → 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 +66 -8
- package/README.md +56 -7
- package/cli/registry.mjs +1 -0
- package/cli/smoke-fixtures.json +45 -3
- package/cli/task.mjs +69 -4
- package/package.json +4 -3
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +4 -11
- package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +11 -12
- package/plugins/gates/hooks/gates/library-docs/index.mjs +107 -31
- package/plugins/gates/hooks/gates/no-trivial-scripts/index.mjs +114 -0
- 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/hooks.json +41 -1
- package/plugins/gates/hooks/lib/hook-io.mjs +5 -2
- package/plugins/gates/hooks/lib/testing.mjs +15 -4
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/lib/task-store.mjs +6 -0
- package/plugins/tasks/hooks/register-requests.mjs +37 -10
- package/registry.json +48 -5
|
@@ -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 |
|
|
@@ -118,6 +120,7 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
|
|
|
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. |
|
|
120
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. |
|
|
121
124
|
|
|
122
125
|
### 🔎 Tool discovery — no reinventar la rueda
|
|
123
126
|
| Gate | | Qué hace |
|
|
@@ -137,6 +140,26 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
|
|
|
137
140
|
|---|---|---|
|
|
138
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. |
|
|
139
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
|
+
|
|
140
163
|
### 🩺 Sesión y contexto — validaciones al arrancar e inyección de capacidades
|
|
141
164
|
| Gate | | Qué hace |
|
|
142
165
|
|---|---|---|
|
|
@@ -160,7 +183,7 @@ ves y editas cada perilla:
|
|
|
160
183
|
```json
|
|
161
184
|
{
|
|
162
185
|
"adopted": "partial",
|
|
163
|
-
"gateVersion": "3.
|
|
186
|
+
"gateVersion": "3.1.0",
|
|
164
187
|
"gates": {
|
|
165
188
|
"blockDestructiveShellCommands": {
|
|
166
189
|
"enabled": true,
|
|
@@ -194,7 +217,9 @@ ves y editas cada perilla:
|
|
|
194
217
|
commit), `lint-ok: <razón>` (un falso positivo documentado del linter), `[skip-lint]`
|
|
195
218
|
(saltea el chequeo de staged-lint por un commit), `[wip]` (permite un commit
|
|
196
219
|
deliberadamente amplio, no atómico), `comment-ok: <razón>` (un comentario explicativo que
|
|
197
|
-
debe quedarse)
|
|
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`.
|
|
198
223
|
- **Inyección de capacidades:** `capability-map` (on por defecto) es totalmente ajustable —
|
|
199
224
|
elegí qué tipos exponer (`"kinds": ["skills", "agents", "commands"]`), limitá cada blurb
|
|
200
225
|
(`maxClauseChars`, default 120), agregá raíces extra por tipo, regulá cada cuánto se
|
|
@@ -231,17 +256,33 @@ El archivo rota una vez a los 5 MB (`gates-log.1.jsonl`). `CLAUDE_GATES_LOG=0` l
|
|
|
231
256
|
|
|
232
257
|
---
|
|
233
258
|
|
|
234
|
-
## Tareas:
|
|
259
|
+
## Tareas: se registran con criterio y se cierran con evidencia verificada
|
|
235
260
|
|
|
236
|
-
|
|
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.
|
|
237
264
|
|
|
238
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>
|
|
239
276
|
claude-gates task close <id> --check "npm test" --expect "fail 0" --note "suite en verde"
|
|
240
277
|
claude-gates task close <id> --exists dist/report.html --contains "All green"
|
|
241
278
|
claude-gates task abandon <id> --reason "obsoleta"
|
|
279
|
+
claude-gates task promote <id> <runId> # vincula la tarea a un run de forge
|
|
242
280
|
```
|
|
243
281
|
|
|
244
|
-
El resultado verificado (comando, código de salida, cola de la salida, fecha) queda guardado
|
|
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.
|
|
245
286
|
|
|
246
287
|
---
|
|
247
288
|
|
|
@@ -263,12 +304,28 @@ claude-gates status # on/off efectivo por gate y su origen (pr
|
|
|
263
304
|
claude-gates log [--tail N] [--deny] [--gate id] [--since iso] [--json]
|
|
264
305
|
claude-gates doctor # ¿Claude Code corre ESTA versión del paquete?
|
|
265
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>
|
|
313
|
+
|
|
266
314
|
# Inspeccionar el catálogo:
|
|
267
315
|
claude-gates registry --list # lista familias y gates
|
|
268
316
|
claude-gates registry --check # valida registry.json y que hooks.json esté sincronizado
|
|
269
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
|
|
270
321
|
```
|
|
271
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
|
+
|
|
272
329
|
---
|
|
273
330
|
|
|
274
331
|
## Estructura del repositorio
|
|
@@ -285,7 +342,8 @@ plugins/gates/ El plugin de gates.
|
|
|
285
342
|
hooks/hooks.json Generado desde registry.json (`registry --sync-hooks`). Lo carga Claude Code.
|
|
286
343
|
hooks/lib/ Código compartido de los hooks (Node built-ins only).
|
|
287
344
|
hooks/gates/<id>/ Un gate por carpeta: index.mjs (la regla) + test.mjs (su test).
|
|
288
|
-
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.
|
|
289
347
|
.claude-plugin/marketplace.json Lista los plugins del marketplace.
|
|
290
348
|
```
|
|
291
349
|
|
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
|
|
|
@@ -120,6 +122,7 @@ works even if you install one on its own.
|
|
|
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. |
|
|
122
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. |
|
|
123
126
|
|
|
124
127
|
### 🔎 Tool discovery — don't reinvent the wheel
|
|
125
128
|
|
|
@@ -141,6 +144,26 @@ works even if you install one on its own.
|
|
|
141
144
|
| ------------ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
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. |
|
|
143
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
|
+
|
|
144
167
|
### 🩺 Session & context — startup checks and capability injection
|
|
145
168
|
|
|
146
169
|
| Gate | | What it does |
|
|
@@ -165,7 +188,7 @@ see and edit every knob:
|
|
|
165
188
|
```json
|
|
166
189
|
{
|
|
167
190
|
"adopted": "partial",
|
|
168
|
-
"gateVersion": "3.
|
|
191
|
+
"gateVersion": "3.1.0",
|
|
169
192
|
"gates": {
|
|
170
193
|
"blockDestructiveShellCommands": {
|
|
171
194
|
"enabled": true,
|
|
@@ -206,7 +229,9 @@ see and edit every knob:
|
|
|
206
229
|
non-memory phrase), `[allow-coauthor]` (one legitimate co-author on a commit),
|
|
207
230
|
`lint-ok: <reason>` (a documented linter false positive), `[skip-lint]` (skip the
|
|
208
231
|
staged-lint check for one commit), `[wip]` (allow one deliberately broad,
|
|
209
|
-
non-atomic commit), `comment-ok: <reason>` (one explanatory comment that must stay)
|
|
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).
|
|
210
235
|
`dependency-skills` opts out via its `depsWithoutOwnApi` list.
|
|
211
236
|
- **Capability injection:** `capability-map` (on by default) is fully tunable — pick which
|
|
212
237
|
kinds to surface (`"kinds": ["skills", "agents", "commands"]`), cap each blurb
|
|
@@ -243,17 +268,33 @@ The file rotates once at 5 MB (`gates-log.1.jsonl`). Set `CLAUDE_GATES_LOG=0` to
|
|
|
243
268
|
|
|
244
269
|
---
|
|
245
270
|
|
|
246
|
-
## Tasks:
|
|
271
|
+
## Tasks: registered with a criterion, closed with verified evidence
|
|
247
272
|
|
|
248
|
-
|
|
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.
|
|
249
276
|
|
|
250
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>
|
|
251
288
|
claude-gates task close <id> --check "npm test" --expect "fail 0" --note "suite green"
|
|
252
289
|
claude-gates task close <id> --exists dist/report.html --contains "All green"
|
|
253
290
|
claude-gates task abandon <id> --reason "obsolete"
|
|
291
|
+
claude-gates task promote <id> <runId> # link the task to a forge run
|
|
254
292
|
```
|
|
255
293
|
|
|
256
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.
|
|
257
298
|
|
|
258
299
|
---
|
|
259
300
|
|
|
@@ -275,6 +316,13 @@ claude-gates status # effective on/off per gate and its source
|
|
|
275
316
|
claude-gates log [--tail N] [--deny] [--gate id] [--since iso] [--json]
|
|
276
317
|
claude-gates doctor # is Claude Code running THIS package version of the plugins?
|
|
277
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>
|
|
325
|
+
|
|
278
326
|
# Inspect the catalog:
|
|
279
327
|
claude-gates registry --list # list families and gates
|
|
280
328
|
claude-gates registry --check # validate registry.json and that hooks.json is in sync
|
|
@@ -306,7 +354,8 @@ plugins/gates/ The gates plugin.
|
|
|
306
354
|
hooks/hooks.json Generated from registry.json (`registry --sync-hooks`). Loaded by Claude Code.
|
|
307
355
|
hooks/lib/ Shared hook code (Node built-ins only).
|
|
308
356
|
hooks/gates/<id>/ One gate per folder: index.mjs (the rule) + test.mjs (its test).
|
|
309
|
-
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.
|
|
310
359
|
.claude-plugin/marketplace.json Lists the marketplace plugins.
|
|
311
360
|
```
|
|
312
361
|
|
package/cli/registry.mjs
CHANGED
package/cli/smoke-fixtures.json
CHANGED
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
{
|
|
137
137
|
"id": "force-parallel",
|
|
138
138
|
"configKey": "warnSequentialDelegations",
|
|
139
|
-
"enabledByDefault":
|
|
140
|
-
"type": "
|
|
139
|
+
"enabledByDefault": true,
|
|
140
|
+
"type": "deny",
|
|
141
141
|
"payload": {
|
|
142
142
|
"tool_name": "Task",
|
|
143
143
|
"tool_input": {
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
}
|
|
146
146
|
},
|
|
147
147
|
"needsState": true,
|
|
148
|
-
"note": "stateful:
|
|
148
|
+
"note": "stateful: denies only on the 3rd consecutive delegation under one session."
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
"id": "feature-catalog",
|
|
@@ -542,6 +542,48 @@
|
|
|
542
542
|
}
|
|
543
543
|
},
|
|
544
544
|
"needsState": false
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"id": "require-task-split",
|
|
548
|
+
"configKey": "requireTaskSplitBeforeImplementing",
|
|
549
|
+
"enabledByDefault": true,
|
|
550
|
+
"type": "deny",
|
|
551
|
+
"payload": {
|
|
552
|
+
"tool_name": "Write",
|
|
553
|
+
"tool_input": {
|
|
554
|
+
"file_path": "src/x.js",
|
|
555
|
+
"content": "x"
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
"needsState": true,
|
|
559
|
+
"note": "needs an .ai/tasks/active.json with a medium+ task that has no children."
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"id": "require-monitor",
|
|
563
|
+
"configKey": "requireMonitorForBackground",
|
|
564
|
+
"enabledByDefault": true,
|
|
565
|
+
"type": "deny",
|
|
566
|
+
"payload": {
|
|
567
|
+
"tool_name": "Bash",
|
|
568
|
+
"tool_input": {
|
|
569
|
+
"command": "npm run build",
|
|
570
|
+
"run_in_background": true
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
"needsState": false
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"id": "no-trivial-scripts",
|
|
577
|
+
"configKey": "blockTrivialInlineScripts",
|
|
578
|
+
"enabledByDefault": true,
|
|
579
|
+
"type": "deny",
|
|
580
|
+
"payload": {
|
|
581
|
+
"tool_name": "Bash",
|
|
582
|
+
"tool_input": {
|
|
583
|
+
"command": "sed -i 's/old/new/' src/a.mjs"
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
"needsState": false
|
|
545
587
|
}
|
|
546
588
|
]
|
|
547
589
|
}
|
package/cli/task.mjs
CHANGED
|
@@ -16,6 +16,12 @@ import { verifyCommand, verifyPath } from './evidence.mjs';
|
|
|
16
16
|
|
|
17
17
|
const DEFAULT_SIZE = 'unspecified';
|
|
18
18
|
|
|
19
|
+
const VERIFY_USAGE =
|
|
20
|
+
'task add requires a deterministic verification criterion: ' +
|
|
21
|
+
'--verify-command "<command>" [--verify-expect <text>] (command that must exit 0 when done) ' +
|
|
22
|
+
'or --verify-path <path> [--verify-contains <text>] (file/dir that must exist when done). ' +
|
|
23
|
+
'This defines HOW the task will be verified as complete — free text is not enough.';
|
|
24
|
+
|
|
19
25
|
function fail(message) {
|
|
20
26
|
process.stderr.write(`${message}\n`);
|
|
21
27
|
process.exit(EXIT_CODE.FAILURE);
|
|
@@ -31,18 +37,41 @@ function openStoreOrFail(cwd) {
|
|
|
31
37
|
return store;
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
function buildVerifyCriteria(options) {
|
|
41
|
+
if (options.verifyCommand) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'command',
|
|
44
|
+
command: options.verifyCommand,
|
|
45
|
+
expect: options.verifyExpect ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (options.verifyPath) {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'path',
|
|
51
|
+
path: options.verifyPath,
|
|
52
|
+
contains: options.verifyContains ?? null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
function taskAdd(title, options, { cwd = process.cwd() } = {}) {
|
|
35
59
|
if (!title || !title.trim()) fail('task add requires a non-empty title.');
|
|
60
|
+
const verify = buildVerifyCriteria(options);
|
|
61
|
+
if (!verify) fail(VERIFY_USAGE);
|
|
36
62
|
const store = openStoreOrFail(cwd);
|
|
37
|
-
const task =
|
|
63
|
+
const task = {
|
|
38
64
|
id: options.id || randomUUID(),
|
|
39
65
|
title: title.trim(),
|
|
40
66
|
description: options.description ?? '',
|
|
41
67
|
status: STATUS.OPEN,
|
|
42
68
|
size: options.size ?? DEFAULT_SIZE,
|
|
69
|
+
verify,
|
|
43
70
|
createdAt: new Date().toISOString(),
|
|
44
71
|
messages: [],
|
|
45
|
-
}
|
|
72
|
+
};
|
|
73
|
+
if (options.parent) task.parentId = options.parent;
|
|
74
|
+
store.add(task);
|
|
46
75
|
process.stdout.write(`${JSON.stringify(task, null, 2)}\n`);
|
|
47
76
|
}
|
|
48
77
|
|
|
@@ -80,9 +109,26 @@ function collectEvidence(options, cwd) {
|
|
|
80
109
|
return null;
|
|
81
110
|
}
|
|
82
111
|
|
|
112
|
+
function autoVerifyFromTask(task, cwd) {
|
|
113
|
+
if (!task?.verify) return null;
|
|
114
|
+
const { kind, command, expect, path, contains } = task.verify;
|
|
115
|
+
if (kind === 'command' && command) {
|
|
116
|
+
return verifyCommand(command, { cwd, expect: expect ?? undefined });
|
|
117
|
+
}
|
|
118
|
+
if (kind === 'path' && path) {
|
|
119
|
+
return verifyPath(path, { cwd, contains: contains ?? undefined });
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
function taskClose(id, options, { cwd = process.cwd() } = {}) {
|
|
84
125
|
const store = openStoreOrFail(cwd);
|
|
85
|
-
|
|
126
|
+
let evidence = collectEvidence(options, store.root);
|
|
127
|
+
if (!evidence && options.evidence !== undefined) fail(EVIDENCE_USAGE);
|
|
128
|
+
if (!evidence) {
|
|
129
|
+
const task = store.active().find((entry) => entry.id === id);
|
|
130
|
+
evidence = autoVerifyFromTask(task, store.root);
|
|
131
|
+
}
|
|
86
132
|
if (!evidence) fail(EVIDENCE_USAGE);
|
|
87
133
|
if (!evidence.verified) {
|
|
88
134
|
const output = evidence.outputTail ? `\n${evidence.outputTail}` : '';
|
|
@@ -121,10 +167,29 @@ export function registerTaskCommand(program) {
|
|
|
121
167
|
|
|
122
168
|
task
|
|
123
169
|
.command('add <title>')
|
|
124
|
-
.description(
|
|
170
|
+
.description(
|
|
171
|
+
'Register a new open task (requires a verification criterion).',
|
|
172
|
+
)
|
|
125
173
|
.option('--id <id>', 'explicit task id (default: a generated uuid)')
|
|
126
174
|
.option('--description <text>', 'longer description of the task')
|
|
127
175
|
.option('--size <size>', 'rough size estimate (e.g. trivial, small, large)')
|
|
176
|
+
.option(
|
|
177
|
+
'--verify-command <command>',
|
|
178
|
+
'shell command that must exit 0 when the task is done',
|
|
179
|
+
)
|
|
180
|
+
.option(
|
|
181
|
+
'--verify-expect <text>',
|
|
182
|
+
'text the --verify-command output must contain',
|
|
183
|
+
)
|
|
184
|
+
.option(
|
|
185
|
+
'--verify-path <path>',
|
|
186
|
+
'file or directory that must exist when the task is done',
|
|
187
|
+
)
|
|
188
|
+
.option(
|
|
189
|
+
'--verify-contains <text>',
|
|
190
|
+
'text the --verify-path file must contain',
|
|
191
|
+
)
|
|
192
|
+
.option('--parent <id>', 'parent task id (makes this a sub-task)')
|
|
128
193
|
.action((title, options) => taskAdd(title, options));
|
|
129
194
|
|
|
130
195
|
task
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devrik-tools/claude-gates",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "49 installable, deterministic gates (hooks) for Claude Code: block destructive commands and protected paths, enforce delegation/spec/quality/research rules, and track tasks that only close with verified evidence. Configurable per project.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"hooks",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"guardrails",
|
|
10
10
|
"pretooluse",
|
|
11
11
|
"policy-enforcement",
|
|
12
|
-
"ai-agent"
|
|
12
|
+
"ai-agent",
|
|
13
|
+
"task-tracking"
|
|
13
14
|
],
|
|
14
15
|
"type": "module",
|
|
15
16
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Deterministic gates for Claude Code: destructive-command blocks, protected paths, delegation briefs, spec-driven flow and session-start validation. Selection lives in config, not in code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|
|
@@ -30,7 +30,6 @@ const CONFIG_KEY = 'requireCircuitBreakerOnDelegation';
|
|
|
30
30
|
const DEFAULT_RETRY_THRESHOLD = 2;
|
|
31
31
|
const MIN_RETRY_THRESHOLD = 2;
|
|
32
32
|
const DEFAULT_SIMILARITY_THRESHOLD = 0.6;
|
|
33
|
-
const MAX_ENTRIES_PER_KEY = 12;
|
|
34
33
|
const MAX_OVERRIDE_SENTENCE_WORDS = 8;
|
|
35
34
|
const MIN_TOKEN_LENGTH = 2;
|
|
36
35
|
|
|
@@ -391,18 +390,12 @@ runGate(
|
|
|
391
390
|
return;
|
|
392
391
|
}
|
|
393
392
|
|
|
393
|
+
// Count includes the current attempt (+1) against stored occurrences.
|
|
394
|
+
// Recording happens in the PostToolUse tracker (track.mjs), not here: a
|
|
395
|
+
// delegation rejected by ANOTHER gate (running in parallel) must not inflate
|
|
396
|
+
// the counter — only delegations that actually launched count as attempts.
|
|
394
397
|
const count =
|
|
395
398
|
1 + similarOccurrenceCount(state, signature, similarityThreshold);
|
|
396
|
-
const occurrences = [
|
|
397
|
-
...occurrencesFor(state, key),
|
|
398
|
-
{ signature, seenAt: Date.now() },
|
|
399
|
-
].slice(-MAX_ENTRIES_PER_KEY);
|
|
400
|
-
writeSessionState(
|
|
401
|
-
GATE_ID,
|
|
402
|
-
sessionId,
|
|
403
|
-
{ ...state, [key]: occurrences },
|
|
404
|
-
stateOptions,
|
|
405
|
-
);
|
|
406
399
|
|
|
407
400
|
const retryThreshold = Math.max(
|
|
408
401
|
MIN_RETRY_THRESHOLD,
|