@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.
@@ -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.8.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.8.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 44 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 (44, en 10 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 |
@@ -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.0.0",
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). `dependency-skills` se exime vía su lista `depsWithoutOwnApi`.
220
+ debe quedarse), `SEQUENTIAL-JUSTIFIED` (una delegación que 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: cerrar exige evidencia verificada
259
+ ## Tareas: se registran con criterio y se cierran con evidencia verificada
235
260
 
236
- `task close` rechaza texto libre. Una tarea está hecha solo cuando una verificación pasa:
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 con la tarea.
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 (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.
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 44 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
 
@@ -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.0.0",
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: closing needs verified evidence
271
+ ## Tasks: registered with a criterion, closed with verified evidence
247
272
 
248
- `task close` refuses free text. A task is done only when a check passes:
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 (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.
310
359
  .claude-plugin/marketplace.json Lists the marketplace plugins.
311
360
  ```
312
361
 
package/cli/registry.mjs CHANGED
@@ -21,6 +21,7 @@ export const TOOL_GROUPS = [
21
21
  'execution',
22
22
  'question',
23
23
  'research',
24
+ 'monitor',
24
25
  ];
25
26
 
26
27
  /** The kinds of value a gate param can take, so the CLI can describe and validate it. */
@@ -136,8 +136,8 @@
136
136
  {
137
137
  "id": "force-parallel",
138
138
  "configKey": "warnSequentialDelegations",
139
- "enabledByDefault": false,
140
- "type": "warn",
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: warns only on the 3rd consecutive delegation under one session."
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 = store.add({
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
- const evidence = collectEvidence(options, store.root);
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('Register a new open task.')
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.8.0",
4
- "description": "Installable, deterministic gates (hooks) for Claude Code: block destructive commands, protected paths, and enforce delegation/spec/quality rules. Configurable per project.",
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.8.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,