@devrik-tools/claude-gates 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.es.md +129 -11
  3. package/README.md +120 -10
  4. package/cli/doctor.mjs +132 -0
  5. package/cli/evidence.mjs +78 -0
  6. package/cli/hooks-manifest.mjs +89 -0
  7. package/cli/index.mjs +124 -6
  8. package/cli/init.mjs +97 -3
  9. package/cli/log.mjs +70 -0
  10. package/cli/materialize.mjs +36 -2
  11. package/cli/registry.mjs +12 -1
  12. package/cli/selection.mjs +18 -0
  13. package/cli/smoke-fixtures.json +159 -25
  14. package/cli/task.mjs +104 -11
  15. package/cli/toggle.mjs +125 -0
  16. package/package.json +4 -3
  17. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  18. package/plugins/gates/hooks/doctor.mjs +47 -1
  19. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
  20. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
  21. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
  22. package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
  23. package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
  24. package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
  25. package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
  26. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
  27. package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
  28. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +94 -215
  29. package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
  30. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
  31. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
  32. package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
  33. package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
  34. package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
  35. package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
  36. package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
  37. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
  38. package/plugins/gates/hooks/gates/force-parallel/index.mjs +53 -95
  39. package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
  40. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
  41. package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
  42. package/plugins/gates/hooks/gates/library-docs/index.mjs +338 -0
  43. package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
  44. package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
  45. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
  46. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
  47. package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
  48. package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
  49. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
  50. package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
  51. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
  52. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
  53. package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
  54. package/plugins/gates/hooks/gates/no-trivial-scripts/index.mjs +114 -0
  55. package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
  56. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
  57. package/plugins/gates/hooks/gates/require-monitor/index.mjs +126 -0
  58. package/plugins/gates/hooks/gates/require-task-split/index.mjs +88 -0
  59. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
  60. package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
  61. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
  62. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
  63. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
  64. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
  65. package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
  66. package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
  67. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
  68. package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
  69. package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
  70. package/plugins/gates/hooks/hooks.json +152 -42
  71. package/plugins/gates/hooks/lib/config.mjs +6 -3
  72. package/plugins/gates/hooks/lib/delegation.mjs +352 -0
  73. package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
  74. package/plugins/gates/hooks/lib/git.mjs +262 -0
  75. package/plugins/gates/hooks/lib/hook-io.mjs +451 -83
  76. package/plugins/gates/hooks/lib/session-state.mjs +150 -0
  77. package/plugins/gates/hooks/lib/signals.mjs +18 -13
  78. package/plugins/gates/hooks/lib/testing.mjs +148 -0
  79. package/plugins/gates/hooks/lib/tools.mjs +144 -0
  80. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  81. package/plugins/tasks/hooks/lib/task-store.mjs +12 -6
  82. package/plugins/tasks/hooks/register-requests.mjs +37 -10
  83. package/registry.json +249 -38
@@ -8,7 +8,9 @@
8
8
  "type": "deny",
9
9
  "payload": {
10
10
  "tool_name": "Bash",
11
- "tool_input": { "command": "git reset --hard HEAD~3" }
11
+ "tool_input": {
12
+ "command": "git reset --hard HEAD~3"
13
+ }
12
14
  },
13
15
  "needsState": false
14
16
  },
@@ -19,7 +21,9 @@
19
21
  "type": "deny",
20
22
  "payload": {
21
23
  "tool_name": "Bash",
22
- "tool_input": { "command": "git push origin main" }
24
+ "tool_input": {
25
+ "command": "git push origin main"
26
+ }
23
27
  },
24
28
  "needsState": false
25
29
  },
@@ -30,7 +34,9 @@
30
34
  "type": "deny",
31
35
  "payload": {
32
36
  "tool_name": "Write",
33
- "tool_input": { "file_path": "/repo/.env" }
37
+ "tool_input": {
38
+ "file_path": "/repo/.env"
39
+ }
34
40
  },
35
41
  "needsState": false
36
42
  },
@@ -41,7 +47,9 @@
41
47
  "type": "deny",
42
48
  "payload": {
43
49
  "tool_name": "Write",
44
- "tool_input": { "file_path": "random-orphan.txt" }
50
+ "tool_input": {
51
+ "file_path": "random-orphan.txt"
52
+ }
45
53
  },
46
54
  "needsState": true,
47
55
  "note": "file_path must resolve against the scratch project root; a literal alone does not reproduce root detection."
@@ -53,7 +61,9 @@
53
61
  "type": "deny",
54
62
  "payload": {
55
63
  "tool_name": "Bash",
56
- "tool_input": { "command": "sleep 30" }
64
+ "tool_input": {
65
+ "command": "sleep 30"
66
+ }
57
67
  },
58
68
  "needsState": false
59
69
  },
@@ -64,7 +74,9 @@
64
74
  "type": "deny",
65
75
  "payload": {
66
76
  "tool_name": "Agent",
67
- "tool_input": { "prompt": "Fix the login bug please, thanks." }
77
+ "tool_input": {
78
+ "prompt": "Fix the login bug please, thanks."
79
+ }
68
80
  },
69
81
  "needsState": false
70
82
  },
@@ -101,7 +113,9 @@
101
113
  "type": "deny",
102
114
  "payload": {
103
115
  "tool_name": "Agent",
104
- "tool_input": { "prompt": "fix the scoring threshold" }
116
+ "tool_input": {
117
+ "prompt": "fix the scoring threshold"
118
+ }
105
119
  },
106
120
  "needsState": true,
107
121
  "note": "stateful: denies only on the 3rd identical retry under the same session id."
@@ -114,7 +128,7 @@
114
128
  "payload": {
115
129
  "tool_name": "Agent",
116
130
  "tool_input": {
117
- "prompt": "Acordate de lo que hablamos antes y aplica el mismo criterio."
131
+ "prompt": "Como te dije antes, aplica el mismo criterio al nuevo archivo."
118
132
  }
119
133
  },
120
134
  "needsState": false
@@ -122,11 +136,16 @@
122
136
  {
123
137
  "id": "force-parallel",
124
138
  "configKey": "warnSequentialDelegations",
125
- "enabledByDefault": false,
126
- "type": "warn",
127
- "payload": { "tool_name": "Task", "tool_input": { "prompt": "do work" } },
139
+ "enabledByDefault": true,
140
+ "type": "deny",
141
+ "payload": {
142
+ "tool_name": "Task",
143
+ "tool_input": {
144
+ "prompt": "do work"
145
+ }
146
+ },
128
147
  "needsState": true,
129
- "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."
130
149
  },
131
150
  {
132
151
  "id": "feature-catalog",
@@ -178,7 +197,9 @@
178
197
  "type": "deny",
179
198
  "payload": {
180
199
  "tool_name": "Agent",
181
- "tool_input": { "prompt": "Nivel: STANDARD\nImplementa el checkout." }
200
+ "tool_input": {
201
+ "prompt": "Nivel: STANDARD\nImplementa el checkout."
202
+ }
182
203
  },
183
204
  "needsState": false
184
205
  },
@@ -189,7 +210,9 @@
189
210
  "type": "deny",
190
211
  "payload": {
191
212
  "tool_name": "Agent",
192
- "tool_input": { "prompt": "Nivel: STANDARD\nImplementa el checkout." }
213
+ "tool_input": {
214
+ "prompt": "Nivel: STANDARD\nImplementa el checkout."
215
+ }
193
216
  },
194
217
  "needsState": false
195
218
  },
@@ -242,7 +265,9 @@
242
265
  "type": "deny",
243
266
  "payload": {
244
267
  "tool_name": "Agent",
245
- "tool_input": { "prompt": "create a new script that checks X" }
268
+ "tool_input": {
269
+ "prompt": "create a new script that checks X"
270
+ }
246
271
  },
247
272
  "needsState": false
248
273
  },
@@ -267,7 +292,9 @@
267
292
  "type": "deny",
268
293
  "payload": {
269
294
  "tool_name": "Bash",
270
- "tool_input": { "command": "echo hi" }
295
+ "tool_input": {
296
+ "command": "echo hi"
297
+ }
271
298
  },
272
299
  "needsState": true,
273
300
  "note": "needs a project rules/ or .claude/skills gate script present that exits non-zero."
@@ -279,7 +306,9 @@
279
306
  "type": "deny",
280
307
  "payload": {
281
308
  "tool_name": "Bash",
282
- "tool_input": { "command": "echo hi" }
309
+ "tool_input": {
310
+ "command": "echo hi"
311
+ }
283
312
  },
284
313
  "needsState": true,
285
314
  "note": "needs .ai/reincidencias.json with an open class at/above threshold."
@@ -307,7 +336,11 @@
307
336
  "payload": {
308
337
  "tool_name": "AskUserQuestion",
309
338
  "tool_input": {
310
- "questions": [{ "question": "Should I migrate the auth schema?" }]
339
+ "questions": [
340
+ {
341
+ "question": "Should I migrate the auth schema?"
342
+ }
343
+ ]
311
344
  }
312
345
  },
313
346
  "needsState": true,
@@ -334,10 +367,11 @@
334
367
  "enabledByDefault": true,
335
368
  "type": "warn",
336
369
  "payload": {
337
- "tool_name": "Write",
370
+ "tool_name": "Edit",
338
371
  "tool_input": {
339
372
  "file_path": "/repo/src/config.js",
340
- "content": "const REQUEST_TIMEOUT_MS = 30000;"
373
+ "old_string": "const REQUEST_TIMEOUT_MS = 30000;",
374
+ "new_string": "const REQUEST_TIMEOUT_MS = 60000;"
341
375
  }
342
376
  },
343
377
  "needsState": false
@@ -349,7 +383,9 @@
349
383
  "type": "deny",
350
384
  "payload": {
351
385
  "tool_name": "Bash",
352
- "tool_input": { "command": "git commit -m x" }
386
+ "tool_input": {
387
+ "command": "git commit -m x"
388
+ }
353
389
  },
354
390
  "needsState": true,
355
391
  "note": "needs a project package.json whose lint script exits non-zero."
@@ -384,7 +420,10 @@
384
420
  "type": "deny",
385
421
  "payload": {
386
422
  "tool_name": "Write",
387
- "tool_input": { "file_path": "src/x.js", "content": "x" }
423
+ "tool_input": {
424
+ "file_path": "src/x.js",
425
+ "content": "x"
426
+ }
388
427
  },
389
428
  "needsState": true,
390
429
  "note": "needs a forge adoption marker + sqlite db with no active run."
@@ -396,7 +435,13 @@
396
435
  "type": "deny",
397
436
  "payload": {
398
437
  "tool_name": "AskUserQuestion",
399
- "tool_input": { "questions": [{ "question": "A or B?" }] }
438
+ "tool_input": {
439
+ "questions": [
440
+ {
441
+ "question": "A or B?"
442
+ }
443
+ ]
444
+ }
400
445
  },
401
446
  "needsState": false
402
447
  },
@@ -434,7 +479,9 @@
434
479
  "type": "deny",
435
480
  "payload": {
436
481
  "tool_name": "Bash",
437
- "tool_input": { "command": "git commit -m \"wip\"" }
482
+ "tool_input": {
483
+ "command": "git commit -m \"wip\""
484
+ }
438
485
  },
439
486
  "needsState": true,
440
487
  "note": "needs a git repo with a staged lintable file that fails lint and a resolvable linter; a bare payload cannot reproduce the staged-file scan."
@@ -446,10 +493,97 @@
446
493
  "type": "deny",
447
494
  "payload": {
448
495
  "tool_name": "Bash",
449
- "tool_input": { "command": "git commit -m \"stuff\"" }
496
+ "tool_input": {
497
+ "command": "git commit -m \"stuff\""
498
+ }
450
499
  },
451
500
  "needsState": true,
452
501
  "note": "needs a git repo with a staged set that mixes >maxNatures natures or exceeds maxFiles; a bare payload cannot reproduce the staged-file scan."
502
+ },
503
+ {
504
+ "id": "engram-first",
505
+ "configKey": "requireEngramBeforeResearch",
506
+ "enabledByDefault": true,
507
+ "type": "deny",
508
+ "payload": {
509
+ "tool_name": "WebSearch",
510
+ "tool_input": {
511
+ "query": "how to use zod refine"
512
+ },
513
+ "session_id": "smoke-engram-first"
514
+ },
515
+ "needsState": false
516
+ },
517
+ {
518
+ "id": "library-docs",
519
+ "configKey": "requireDocsBeforeUsingNewLibrary",
520
+ "enabledByDefault": true,
521
+ "type": "deny",
522
+ "payload": {
523
+ "tool_name": "Write",
524
+ "tool_input": {
525
+ "file_path": "src/schema.ts",
526
+ "content": "import { z } from 'zod';\n"
527
+ },
528
+ "session_id": "smoke-library-docs"
529
+ },
530
+ "needsState": false
531
+ },
532
+ {
533
+ "id": "no-explanatory-comments",
534
+ "configKey": "blockExplanatoryComments",
535
+ "enabledByDefault": true,
536
+ "type": "deny",
537
+ "payload": {
538
+ "tool_name": "Write",
539
+ "tool_input": {
540
+ "file_path": "src/a.mjs",
541
+ "content": "// reads the config and returns it\nexport const x = 1;\n"
542
+ }
543
+ },
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
453
587
  }
454
588
  ]
455
589
  }
package/cli/task.mjs CHANGED
@@ -12,9 +12,16 @@ import {
12
12
  STATUS,
13
13
  } from '../plugins/tasks/hooks/lib/task-store.mjs';
14
14
  import { EXIT_CODE } from './constants.mjs';
15
+ import { verifyCommand, verifyPath } from './evidence.mjs';
15
16
 
16
17
  const DEFAULT_SIZE = 'unspecified';
17
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
+
18
25
  function fail(message) {
19
26
  process.stderr.write(`${message}\n`);
20
27
  process.exit(EXIT_CODE.FAILURE);
@@ -30,18 +37,41 @@ function openStoreOrFail(cwd) {
30
37
  return store;
31
38
  }
32
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
+
33
58
  function taskAdd(title, options, { cwd = process.cwd() } = {}) {
34
59
  if (!title || !title.trim()) fail('task add requires a non-empty title.');
60
+ const verify = buildVerifyCriteria(options);
61
+ if (!verify) fail(VERIFY_USAGE);
35
62
  const store = openStoreOrFail(cwd);
36
- const task = store.add({
63
+ const task = {
37
64
  id: options.id || randomUUID(),
38
65
  title: title.trim(),
39
66
  description: options.description ?? '',
40
67
  status: STATUS.OPEN,
41
68
  size: options.size ?? DEFAULT_SIZE,
69
+ verify,
42
70
  createdAt: new Date().toISOString(),
43
71
  messages: [],
44
- });
72
+ };
73
+ if (options.parent) task.parentId = options.parent;
74
+ store.add(task);
45
75
  process.stdout.write(`${JSON.stringify(task, null, 2)}\n`);
46
76
  }
47
77
 
@@ -64,19 +94,53 @@ function reportCloseResult(result) {
64
94
  process.stdout.write(`${JSON.stringify(result.task, null, 2)}\n`);
65
95
  }
66
96
 
97
+ const EVIDENCE_USAGE =
98
+ 'closing a task as done requires verified evidence: --check "<command>" ' +
99
+ '[--expect <text>] (the command must exit 0) or --exists <path> [--contains <text>]. ' +
100
+ 'Free text (--evidence/--note) is only a note. Use `task abandon` if it will not be finished.';
101
+
102
+ function collectEvidence(options, cwd) {
103
+ if (options.check) {
104
+ return verifyCommand(options.check, { cwd, expect: options.expect });
105
+ }
106
+ if (options.exists) {
107
+ return verifyPath(options.exists, { cwd, contains: options.contains });
108
+ }
109
+ return null;
110
+ }
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
+
67
124
  function taskClose(id, options, { cwd = process.cwd() } = {}) {
68
- if (!options.evidence || !options.evidence.trim()) {
125
+ const store = openStoreOrFail(cwd);
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
+ }
132
+ if (!evidence) fail(EVIDENCE_USAGE);
133
+ if (!evidence.verified) {
134
+ const output = evidence.outputTail ? `\n${evidence.outputTail}` : '';
69
135
  fail(
70
- 'closing a task as done requires evidence that it was attended and resolved ' +
71
- '(test output, a diff, a verification note). Pass --evidence, or use ' +
72
- '`task abandon` if it will not be finished.',
136
+ `evidence did not verify (${evidence.failure}); the task stays open.${output}`,
73
137
  );
74
138
  }
75
- const store = openStoreOrFail(cwd);
139
+ const note = options.note ?? options.evidence ?? '';
76
140
  reportCloseResult(
77
141
  store.close(id, STATUS.DONE, {
78
142
  reason: options.reason ?? '',
79
- evidence: options.evidence,
143
+ evidence: note ? { ...evidence, note } : evidence,
80
144
  }),
81
145
  );
82
146
  }
@@ -103,10 +167,29 @@ export function registerTaskCommand(program) {
103
167
 
104
168
  task
105
169
  .command('add <title>')
106
- .description('Register a new open task.')
170
+ .description(
171
+ 'Register a new open task (requires a verification criterion).',
172
+ )
107
173
  .option('--id <id>', 'explicit task id (default: a generated uuid)')
108
174
  .option('--description <text>', 'longer description of the task')
109
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)')
110
193
  .action((title, options) => taskAdd(title, options));
111
194
 
112
195
  task
@@ -117,10 +200,20 @@ export function registerTaskCommand(program) {
117
200
 
118
201
  task
119
202
  .command('close <id>')
120
- .description('Close a task as done. Requires --evidence.')
203
+ .description(
204
+ 'Close a task as done. Requires verified evidence: --check or --exists.',
205
+ )
206
+ .option(
207
+ '--check <command>',
208
+ 'command that must exit 0 (run at the project root)',
209
+ )
210
+ .option('--expect <text>', 'text the --check output must contain')
211
+ .option('--exists <path>', 'file or directory that must exist')
212
+ .option('--contains <text>', 'text the --exists file must contain')
213
+ .option('--note <text>', 'free-text context stored with the evidence')
121
214
  .option(
122
215
  '--evidence <text>',
123
- 'proof the task was attended and resolved (required)',
216
+ 'alias of --note (free text alone is not evidence)',
124
217
  )
125
218
  .option('--reason <text>', 'closing note')
126
219
  .action((id, options) => taskClose(id, options));
package/cli/toggle.mjs ADDED
@@ -0,0 +1,125 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readJsonOrNull } from '../plugins/gates/hooks/lib/config.mjs';
3
+ import { SCOPES, configPathFor, readConfig, writeConfig } from './config.mjs';
4
+ import { defaultParametersOf } from './materialize.mjs';
5
+ import { allGates } from './registry.mjs';
6
+ import { adoptionOf } from './selection.mjs';
7
+
8
+ const ALL_TARGET = 'all';
9
+
10
+ export function resolveTargets(registry, ids) {
11
+ const gates = allGates(registry);
12
+ const families = new Set(registry.families.map((family) => family.id));
13
+ const keys = new Set();
14
+ const unknown = [];
15
+ for (const id of ids) {
16
+ if (id === ALL_TARGET) {
17
+ for (const gate of gates) keys.add(gate.configKey);
18
+ continue;
19
+ }
20
+ if (families.has(id)) {
21
+ for (const gate of gates.filter((entry) => entry.family === id))
22
+ keys.add(gate.configKey);
23
+ continue;
24
+ }
25
+ const gate = gates.find(
26
+ (entry) => entry.id === id || entry.configKey === id,
27
+ );
28
+ if (gate) keys.add(gate.configKey);
29
+ else unknown.push(id);
30
+ }
31
+ if (unknown.length > 0)
32
+ throw new Error(
33
+ `Unknown gate, family or config key: ${unknown.join(', ')}. Run \`claude-gates registry --list\` to see the ids.`,
34
+ );
35
+ return [...keys];
36
+ }
37
+
38
+ export function enabledOf(entry, fallback) {
39
+ if (typeof entry === 'boolean') return entry;
40
+ if (entry && typeof entry === 'object')
41
+ return entry.enabled === undefined ? fallback : entry.enabled !== false;
42
+ return fallback;
43
+ }
44
+
45
+ function entryWithEnabled(gate, existing, enabled) {
46
+ if (existing && typeof existing === 'object') return { ...existing, enabled };
47
+ const hasParameters = Array.isArray(gate.params) && gate.params.length > 0;
48
+ if (!hasParameters) return enabled;
49
+ return { enabled, ...defaultParametersOf(gate) };
50
+ }
51
+
52
+ function adoptionFromGates(registry, gates) {
53
+ const map = {};
54
+ for (const gate of allGates(registry))
55
+ map[gate.configKey] = enabledOf(gates[gate.configKey], gate.default);
56
+ return adoptionOf(map);
57
+ }
58
+
59
+ export function toggleGates(path, registry, configKeys, enabled) {
60
+ const existing = readConfig(path);
61
+ if (existing.corrupt)
62
+ throw new Error(`${path} exists but is not valid JSON. Fix it first.`);
63
+ const gates = { ...(existing.data.gates ?? {}) };
64
+ const changed = [];
65
+ for (const gate of allGates(registry)) {
66
+ if (!configKeys.includes(gate.configKey)) continue;
67
+ const before = enabledOf(gates[gate.configKey], gate.default);
68
+ gates[gate.configKey] = entryWithEnabled(
69
+ gate,
70
+ gates[gate.configKey],
71
+ enabled,
72
+ );
73
+ if (before !== enabled) changed.push(gate.configKey);
74
+ }
75
+ const next = {
76
+ ...existing.data,
77
+ adopted: adoptionFromGates(registry, gates),
78
+ gateVersion: registry.gateVersion,
79
+ gates,
80
+ };
81
+ writeConfig(path, next);
82
+ return { path, changed, unchanged: configKeys.length - changed.length };
83
+ }
84
+
85
+ export function defaultScopeFor(cwd) {
86
+ if (existsSync(configPathFor(SCOPES.PROJECT, { cwd }))) return SCOPES.PROJECT;
87
+ if (existsSync(configPathFor(SCOPES.GLOBAL))) return SCOPES.GLOBAL;
88
+ return SCOPES.PROJECT;
89
+ }
90
+
91
+ function effectiveLayer(projectConfig, globalConfig) {
92
+ if (projectConfig)
93
+ return { name: SCOPES.PROJECT, gates: projectConfig.gates ?? {} };
94
+ if (globalConfig)
95
+ return { name: SCOPES.GLOBAL, gates: globalConfig.gates ?? {} };
96
+ return { name: 'default', gates: {} };
97
+ }
98
+
99
+ export function statusRows(registry, { cwd = process.cwd() } = {}) {
100
+ const projectPath = configPathFor(SCOPES.PROJECT, { cwd });
101
+ const globalPath = configPathFor(SCOPES.GLOBAL);
102
+ const projectConfig = readJsonOrNull(projectPath);
103
+ const globalConfig = readJsonOrNull(globalPath);
104
+ const layer = effectiveLayer(projectConfig, globalConfig);
105
+ return allGates(registry).map((gate) => {
106
+ const entry = layer.gates[gate.configKey];
107
+ const declared = entry !== undefined;
108
+ return {
109
+ id: gate.id,
110
+ configKey: gate.configKey,
111
+ family: gate.family,
112
+ enabled: enabledOf(entry, gate.default),
113
+ source: declared ? layer.name : 'default',
114
+ };
115
+ });
116
+ }
117
+
118
+ export function renderStatus(rows, layerLabel) {
119
+ const width = Math.max(...rows.map((row) => row.id.length));
120
+ const lines = rows.map(
121
+ (row) =>
122
+ `${row.enabled ? 'on ' : 'off'} ${row.id.padEnd(width)} ${row.configKey} (${row.source})`,
123
+ );
124
+ return `${layerLabel}\n${lines.join('\n')}`;
125
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devrik-tools/claude-gates",
3
- "version": "0.7.2",
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.7.2",
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"