@devrik-tools/claude-gates 0.7.2 → 0.8.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 +2 -2
- package/README.es.md +69 -9
- package/README.md +68 -7
- package/cli/doctor.mjs +132 -0
- package/cli/evidence.mjs +78 -0
- package/cli/hooks-manifest.mjs +89 -0
- package/cli/index.mjs +124 -6
- package/cli/init.mjs +97 -3
- package/cli/log.mjs +70 -0
- package/cli/materialize.mjs +36 -2
- package/cli/registry.mjs +11 -1
- package/cli/selection.mjs +18 -0
- package/cli/smoke-fixtures.json +114 -22
- package/cli/task.mjs +36 -8
- package/cli/toggle.mjs +125 -0
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/doctor.mjs +47 -1
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +92 -119
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +101 -66
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -8
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +13 -64
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +186 -163
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +100 -101
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +71 -140
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +56 -137
- package/plugins/gates/hooks/gates/capability-map/index.mjs +280 -506
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +98 -212
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +127 -46
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +81 -19
- package/plugins/gates/hooks/gates/engram-first/index.mjs +41 -0
- package/plugins/gates/hooks/gates/engram-first/session-start.mjs +64 -0
- package/plugins/gates/hooks/gates/engram-first/shared.mjs +90 -0
- package/plugins/gates/hooks/gates/engram-first/stop.mjs +23 -0
- package/plugins/gates/hooks/gates/engram-first/track.mjs +97 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +156 -50
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +51 -92
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +51 -83
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +40 -80
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +23 -149
- package/plugins/gates/hooks/gates/library-docs/index.mjs +262 -0
- package/plugins/gates/hooks/gates/library-docs/track.mjs +95 -0
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +88 -97
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +48 -95
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +69 -31
- package/plugins/gates/hooks/gates/never-assume/index.mjs +23 -18
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +127 -86
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +108 -88
- package/plugins/gates/hooks/gates/no-explanatory-comments/index.mjs +281 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +170 -110
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +46 -77
- package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +85 -47
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +137 -90
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +95 -51
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +72 -216
- package/plugins/gates/hooks/gates/risk-level/index.mjs +51 -204
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +34 -17
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +160 -95
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +196 -97
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +90 -143
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +98 -106
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +47 -148
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +78 -68
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +53 -88
- package/plugins/gates/hooks/gates/tool-map/index.mjs +80 -110
- package/plugins/gates/hooks/hooks.json +112 -42
- package/plugins/gates/hooks/lib/config.mjs +6 -3
- package/plugins/gates/hooks/lib/delegation.mjs +352 -0
- package/plugins/gates/hooks/lib/gate-log.mjs +141 -0
- package/plugins/gates/hooks/lib/git.mjs +262 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +448 -83
- package/plugins/gates/hooks/lib/session-state.mjs +150 -0
- package/plugins/gates/hooks/lib/signals.mjs +18 -13
- package/plugins/gates/hooks/lib/testing.mjs +137 -0
- package/plugins/gates/hooks/lib/tools.mjs +144 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/lib/task-store.mjs +6 -6
- package/registry.json +204 -36
package/cli/smoke-fixtures.json
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"type": "deny",
|
|
9
9
|
"payload": {
|
|
10
10
|
"tool_name": "Bash",
|
|
11
|
-
"tool_input": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": "
|
|
131
|
+
"prompt": "Como te dije antes, aplica el mismo criterio al nuevo archivo."
|
|
118
132
|
}
|
|
119
133
|
},
|
|
120
134
|
"needsState": false
|
|
@@ -124,7 +138,12 @@
|
|
|
124
138
|
"configKey": "warnSequentialDelegations",
|
|
125
139
|
"enabledByDefault": false,
|
|
126
140
|
"type": "warn",
|
|
127
|
-
"payload": {
|
|
141
|
+
"payload": {
|
|
142
|
+
"tool_name": "Task",
|
|
143
|
+
"tool_input": {
|
|
144
|
+
"prompt": "do work"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
128
147
|
"needsState": true,
|
|
129
148
|
"note": "stateful: warns only on the 3rd consecutive delegation under one session."
|
|
130
149
|
},
|
|
@@ -178,7 +197,9 @@
|
|
|
178
197
|
"type": "deny",
|
|
179
198
|
"payload": {
|
|
180
199
|
"tool_name": "Agent",
|
|
181
|
-
"tool_input": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": [
|
|
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": "
|
|
370
|
+
"tool_name": "Edit",
|
|
338
371
|
"tool_input": {
|
|
339
372
|
"file_path": "/repo/src/config.js",
|
|
340
|
-
"
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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,55 @@
|
|
|
446
493
|
"type": "deny",
|
|
447
494
|
"payload": {
|
|
448
495
|
"tool_name": "Bash",
|
|
449
|
-
"tool_input": {
|
|
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
|
|
453
545
|
}
|
|
454
546
|
]
|
|
455
547
|
}
|
package/cli/task.mjs
CHANGED
|
@@ -12,6 +12,7 @@ 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
|
|
|
@@ -64,19 +65,36 @@ function reportCloseResult(result) {
|
|
|
64
65
|
process.stdout.write(`${JSON.stringify(result.task, null, 2)}\n`);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
const EVIDENCE_USAGE =
|
|
69
|
+
'closing a task as done requires verified evidence: --check "<command>" ' +
|
|
70
|
+
'[--expect <text>] (the command must exit 0) or --exists <path> [--contains <text>]. ' +
|
|
71
|
+
'Free text (--evidence/--note) is only a note. Use `task abandon` if it will not be finished.';
|
|
72
|
+
|
|
73
|
+
function collectEvidence(options, cwd) {
|
|
74
|
+
if (options.check) {
|
|
75
|
+
return verifyCommand(options.check, { cwd, expect: options.expect });
|
|
76
|
+
}
|
|
77
|
+
if (options.exists) {
|
|
78
|
+
return verifyPath(options.exists, { cwd, contains: options.contains });
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
function taskClose(id, options, { cwd = process.cwd() } = {}) {
|
|
68
|
-
|
|
84
|
+
const store = openStoreOrFail(cwd);
|
|
85
|
+
const evidence = collectEvidence(options, store.root);
|
|
86
|
+
if (!evidence) fail(EVIDENCE_USAGE);
|
|
87
|
+
if (!evidence.verified) {
|
|
88
|
+
const output = evidence.outputTail ? `\n${evidence.outputTail}` : '';
|
|
69
89
|
fail(
|
|
70
|
-
|
|
71
|
-
'(test output, a diff, a verification note). Pass --evidence, or use ' +
|
|
72
|
-
'`task abandon` if it will not be finished.',
|
|
90
|
+
`evidence did not verify (${evidence.failure}); the task stays open.${output}`,
|
|
73
91
|
);
|
|
74
92
|
}
|
|
75
|
-
const
|
|
93
|
+
const note = options.note ?? options.evidence ?? '';
|
|
76
94
|
reportCloseResult(
|
|
77
95
|
store.close(id, STATUS.DONE, {
|
|
78
96
|
reason: options.reason ?? '',
|
|
79
|
-
evidence:
|
|
97
|
+
evidence: note ? { ...evidence, note } : evidence,
|
|
80
98
|
}),
|
|
81
99
|
);
|
|
82
100
|
}
|
|
@@ -117,10 +135,20 @@ export function registerTaskCommand(program) {
|
|
|
117
135
|
|
|
118
136
|
task
|
|
119
137
|
.command('close <id>')
|
|
120
|
-
.description(
|
|
138
|
+
.description(
|
|
139
|
+
'Close a task as done. Requires verified evidence: --check or --exists.',
|
|
140
|
+
)
|
|
141
|
+
.option(
|
|
142
|
+
'--check <command>',
|
|
143
|
+
'command that must exit 0 (run at the project root)',
|
|
144
|
+
)
|
|
145
|
+
.option('--expect <text>', 'text the --check output must contain')
|
|
146
|
+
.option('--exists <path>', 'file or directory that must exist')
|
|
147
|
+
.option('--contains <text>', 'text the --exists file must contain')
|
|
148
|
+
.option('--note <text>', 'free-text context stored with the evidence')
|
|
121
149
|
.option(
|
|
122
150
|
'--evidence <text>',
|
|
123
|
-
'
|
|
151
|
+
'alias of --note (free text alone is not evidence)',
|
|
124
152
|
)
|
|
125
153
|
.option('--reason <text>', 'closing note')
|
|
126
154
|
.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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devrik-tools/claude-gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Installable, deterministic gates (hooks) for Claude Code: block destructive commands, protected paths, and enforce delegation/spec/quality rules. Configurable per project.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Fail-safe: every check is wrapped so a doctor bug never blocks a session from
|
|
16
16
|
// starting — worst case it silently skips a check rather than throwing.
|
|
17
17
|
|
|
18
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
@@ -29,6 +29,14 @@ const HOOKS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
|
29
29
|
const REPOSITORY_ROOT = join(HOOKS_DIRECTORY, '..', '..', '..');
|
|
30
30
|
const PACKAGE_JSON_PATH = join(REPOSITORY_ROOT, 'package.json');
|
|
31
31
|
const REGISTRY_PATH = join(REPOSITORY_ROOT, 'registry.json');
|
|
32
|
+
const PLUGIN_MANIFEST_PATH = join(
|
|
33
|
+
HOOKS_DIRECTORY,
|
|
34
|
+
'..',
|
|
35
|
+
'.claude-plugin',
|
|
36
|
+
'plugin.json',
|
|
37
|
+
);
|
|
38
|
+
const PLUGIN_CACHE = join(homedir(), '.claude', 'plugins', 'cache');
|
|
39
|
+
const PLUGIN_NAME = 'gates';
|
|
32
40
|
|
|
33
41
|
// Same config lookup as the other session hooks (see session-tasks.mjs / register-requests.mjs):
|
|
34
42
|
// kept local and dependency-free so this plugin works standalone.
|
|
@@ -126,6 +134,41 @@ function minNodeVersionRequired(configuredMin) {
|
|
|
126
134
|
return token ?? DEFAULT_MIN_NODE_VERSION;
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
function directoriesIn(path) {
|
|
138
|
+
try {
|
|
139
|
+
return readdirSync(path, { withFileTypes: true })
|
|
140
|
+
.filter((entry) => entry.isDirectory())
|
|
141
|
+
.map((entry) => entry.name);
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A newer gates version sitting in Claude Code's plugin cache means this session runs a
|
|
148
|
+
* stale install (the marketplace still points at an old package path). */
|
|
149
|
+
function staleInstallProblem() {
|
|
150
|
+
const running = readJson(PLUGIN_MANIFEST_PATH)?.version;
|
|
151
|
+
if (!parseVersion(running)) return null;
|
|
152
|
+
let newest = running;
|
|
153
|
+
let marketplace = null;
|
|
154
|
+
for (const marketplaceName of directoriesIn(PLUGIN_CACHE)) {
|
|
155
|
+
for (const version of directoriesIn(
|
|
156
|
+
join(PLUGIN_CACHE, marketplaceName, PLUGIN_NAME),
|
|
157
|
+
)) {
|
|
158
|
+
if (parseVersion(version) && !versionAtLeast(newest, version)) {
|
|
159
|
+
newest = version;
|
|
160
|
+
marketplace = marketplaceName;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (newest === running) return null;
|
|
165
|
+
return (
|
|
166
|
+
`the gates plugin running in this session is ${running} but ${newest} is installed in the ` +
|
|
167
|
+
`plugin cache (${marketplace}). Run \`claude plugin update ${PLUGIN_NAME}@${marketplace}\` or ` +
|
|
168
|
+
'`claude-gates init --global --defaults --yes` from the newer package, then restart the session.'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
function checkNodeVersion(minVersion) {
|
|
130
173
|
if (versionAtLeast(process.version, minVersion)) return null;
|
|
131
174
|
return (
|
|
@@ -184,6 +227,9 @@ function main() {
|
|
|
184
227
|
const versionProblem = checkNodeVersion(minVersion);
|
|
185
228
|
if (versionProblem) problems.push(versionProblem);
|
|
186
229
|
|
|
230
|
+
const staleInstall = staleInstallProblem();
|
|
231
|
+
if (staleInstall) problems.push(staleInstall);
|
|
232
|
+
|
|
187
233
|
const missingScripts = missingFolderGateScripts();
|
|
188
234
|
if (missingScripts.length > 0) {
|
|
189
235
|
problems.push(
|