@devrik-tools/claude-gates 0.3.1 → 0.6.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 (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +25 -4
  3. package/README.md +75 -43
  4. package/cli/config.mjs +126 -124
  5. package/cli/index.mjs +154 -104
  6. package/cli/init.mjs +303 -276
  7. package/cli/install.mjs +281 -175
  8. package/cli/materialize.mjs +103 -102
  9. package/cli/registry.mjs +139 -136
  10. package/cli/smoke-fixtures.json +441 -0
  11. package/cli/smoke.mjs +129 -0
  12. package/cli/task.mjs +140 -140
  13. package/package.json +1 -1
  14. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  15. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  16. package/plugins/gates/hooks/doctor.mjs +207 -207
  17. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  18. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  19. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  20. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  21. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  22. package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
  23. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  24. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  25. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  26. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  27. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  28. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  29. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  30. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  31. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  32. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  33. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  34. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  35. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  36. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  37. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  38. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  39. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  40. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  41. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  42. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  43. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  44. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  45. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  46. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  47. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  48. package/plugins/gates/hooks/hooks.json +51 -0
  49. package/plugins/gates/hooks/lib/config.mjs +179 -172
  50. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  51. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  52. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  53. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  54. package/plugins/tasks/hooks/hooks.json +26 -26
  55. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  56. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  57. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  58. package/registry.json +171 -1
package/cli/index.mjs CHANGED
@@ -1,104 +1,154 @@
1
- #!/usr/bin/env node
2
- // Entry point (commander). Commands:
3
- // init interactive (or flag-driven) selection of gates, per project or globally
4
- // registry --check validates registry.json; --list prints the catalog
5
-
6
- import { readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { Command } from 'commander';
9
- import {
10
- EXIT_CODE,
11
- LIST_SEPARATOR,
12
- REGISTRY_PATH,
13
- REPOSITORY_ROOT,
14
- } from './constants.mjs';
15
- import { loadRegistry, validateRegistry } from './registry.mjs';
16
- import { registerTaskCommand } from './task.mjs';
17
-
18
- const packageManifest = JSON.parse(
19
- readFileSync(join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
20
- );
21
-
22
- function fail(message) {
23
- process.stderr.write(`${message}\n`);
24
- process.exit(EXIT_CODE.FAILURE);
25
- }
26
-
27
- function splitList(value) {
28
- return value
29
- .split(LIST_SEPARATOR)
30
- .map((item) => item.trim())
31
- .filter(Boolean);
32
- }
33
-
34
- function registryCheck() {
35
- const problems = validateRegistry(
36
- JSON.parse(readFileSync(REGISTRY_PATH, 'utf8')),
37
- );
38
- if (problems.length > 0) {
39
- fail(`registry.json is invalid:\n- ${problems.join('\n- ')}`);
40
- }
41
- process.stdout.write('registry.json OK\n');
42
- }
43
-
44
- function registryList() {
45
- const registry = loadRegistry();
46
- for (const family of registry.families) {
47
- process.stdout.write(`${family.name} (${family.id})\n`);
48
- for (const gate of family.gates) {
49
- const marker = gate.default ? '*' : ' ';
50
- process.stdout.write(` ${marker} ${gate.id} ${gate.configKey}\n`);
51
- }
52
- }
53
- process.stdout.write('\n* = recommended default\n');
54
- }
55
-
56
- const program = new Command()
57
- .name(packageManifest.name)
58
- .description(packageManifest.description)
59
- .version(packageManifest.version);
60
-
61
- program
62
- .command('init')
63
- .description('Choose which gates to adopt and write the config.')
64
- .option('--project', 'apply to this project (<root>/.ai/config.json)')
65
- .option('--global', 'apply globally (~/.claude/claude-gates/config.json)')
66
- .option('--defaults', 'enable the recommended defaults')
67
- .option('--all', 'enable every gate')
68
- .option('--none', "record a 'no' so you are not asked again")
69
- .option(
70
- '--families <ids>',
71
- 'enable whole families, comma-separated',
72
- splitList,
73
- )
74
- .option(
75
- '--gates <ids>',
76
- 'enable individual gates, comma-separated',
77
- splitList,
78
- )
79
- .option('-y, --yes', 'never prompt; use flags and defaults')
80
- .option('--no-install', 'write the config but do not install the plugin')
81
- .option(
82
- '--no-remove-previous',
83
- 'keep any previously installed plugin version instead of removing it first',
84
- )
85
- .option('--dry-run', 'show the selection without writing')
86
- .action(async (options) => {
87
- const { runInit } = await import('./init.mjs');
88
- await runInit(options);
89
- });
90
-
91
- program
92
- .command('registry')
93
- .description('Inspect registry.json.')
94
- .option('--check', 'validate the registry')
95
- .option('--list', 'print families and gates')
96
- .action((options) => {
97
- if (options.check) return registryCheck();
98
- if (options.list) return registryList();
99
- return fail('registry needs --check or --list');
100
- });
101
-
102
- registerTaskCommand(program);
103
-
104
- program.parseAsync(process.argv).catch((error) => fail(error.message));
1
+ #!/usr/bin/env node
2
+ // Entry point (commander). Commands:
3
+ // init interactive (or flag-driven) selection of gates, per project or globally
4
+ // registry --check validates registry.json; --list prints the catalog
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { Command } from 'commander';
9
+ import {
10
+ EXIT_CODE,
11
+ LIST_SEPARATOR,
12
+ REGISTRY_PATH,
13
+ REPOSITORY_ROOT,
14
+ } from './constants.mjs';
15
+ import { loadRegistry, validateRegistry } from './registry.mjs';
16
+ import { runSmoke, OUTCOMES } from './smoke.mjs';
17
+ import { registerTaskCommand } from './task.mjs';
18
+
19
+ const packageManifest = JSON.parse(
20
+ readFileSync(join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
21
+ );
22
+
23
+ function fail(message) {
24
+ process.stderr.write(`${message}\n`);
25
+ process.exit(EXIT_CODE.FAILURE);
26
+ }
27
+
28
+ function splitList(value) {
29
+ return value
30
+ .split(LIST_SEPARATOR)
31
+ .map((item) => item.trim())
32
+ .filter(Boolean);
33
+ }
34
+
35
+ function registryCheck() {
36
+ const problems = validateRegistry(
37
+ JSON.parse(readFileSync(REGISTRY_PATH, 'utf8')),
38
+ );
39
+ if (problems.length > 0) {
40
+ fail(`registry.json is invalid:\n- ${problems.join('\n- ')}`);
41
+ }
42
+ process.stdout.write('registry.json OK\n');
43
+ }
44
+
45
+ function registryList() {
46
+ const registry = loadRegistry();
47
+ for (const family of registry.families) {
48
+ process.stdout.write(`${family.name} (${family.id})\n`);
49
+ for (const gate of family.gates) {
50
+ const marker = gate.default ? '*' : ' ';
51
+ process.stdout.write(` ${marker} ${gate.id} ${gate.configKey}\n`);
52
+ }
53
+ }
54
+ process.stdout.write('\n* = recommended default\n');
55
+ }
56
+
57
+ const SMOKE_FIXTURES_PATH = join(REPOSITORY_ROOT, 'cli', 'smoke-fixtures.json');
58
+ // Width the gate id is padded to so the expected/got columns line up in the report.
59
+ const GATE_ID_COLUMN_WIDTH = 28;
60
+ const OUTCOME_MARK = {
61
+ [OUTCOMES.REACTED]: 'ok ',
62
+ [OUTCOMES.NO_REACTION]: 'FAIL',
63
+ [OUTCOMES.SKIPPED]: 'skip',
64
+ [OUTCOMES.ERROR]: 'ERR ',
65
+ };
66
+
67
+ // Runs each gate against a known violation and reports whether it actually reacts. Unlike
68
+ // `registry --check` (structure) and the doctor hook (files exist), this confirms behavior:
69
+ // it is the check that catches a gate silently allowing its own known violation. Exits
70
+ // non-zero if any gate did not react (a real defect) — so CI or a post-install step can gate
71
+ // on it. Gates whose violation needs seeded state are reported `skip`, never counted as pass.
72
+ function smokeGates() {
73
+ const manifest = JSON.parse(
74
+ readFileSync(SMOKE_FIXTURES_PATH, 'utf8'),
75
+ ).fixtures;
76
+ const { results, tally } = runSmoke(manifest);
77
+
78
+ for (const result of results) {
79
+ const detail = result.reason ? ` (${result.reason})` : '';
80
+ process.stdout.write(
81
+ `${OUTCOME_MARK[result.outcome]} ${result.id.padEnd(GATE_ID_COLUMN_WIDTH)} ` +
82
+ `expected=${result.expected} got=${result.got ?? '-'}${detail}\n`,
83
+ );
84
+ }
85
+ process.stdout.write(
86
+ `\nreacted ${tally.reacted} · no-reaction ${tally['no-reaction']} · ` +
87
+ `skipped ${tally.skipped} · error ${tally.error}\n`,
88
+ );
89
+ if (tally['no-reaction'] > 0 || tally.error > 0) {
90
+ process.stdout.write(
91
+ '\nSome gates did not react to their own known violation. This is a defect: ' +
92
+ 'the gate is wired but does not block/warn as declared.\n',
93
+ );
94
+ process.exit(EXIT_CODE.FAILURE);
95
+ }
96
+ }
97
+
98
+ const program = new Command()
99
+ .name(packageManifest.name)
100
+ .description(packageManifest.description)
101
+ .version(packageManifest.version);
102
+
103
+ program
104
+ .command('init')
105
+ .description('Choose which gates to adopt and write the config.')
106
+ .option('--project', 'apply to this project (<root>/.ai/config.json)')
107
+ .option('--global', 'apply globally (~/.claude/claude-gates/config.json)')
108
+ .option('--defaults', 'enable the recommended defaults')
109
+ .option('--all', 'enable every gate')
110
+ .option('--none', "record a 'no' so you are not asked again")
111
+ .option(
112
+ '--families <ids>',
113
+ 'enable whole families, comma-separated',
114
+ splitList,
115
+ )
116
+ .option(
117
+ '--gates <ids>',
118
+ 'enable individual gates, comma-separated',
119
+ splitList,
120
+ )
121
+ .option('-y, --yes', 'never prompt; use flags and defaults')
122
+ .option('--no-install', 'write the config but do not install the plugin')
123
+ .option(
124
+ '--no-remove-previous',
125
+ 'keep any previously installed plugin version instead of removing it first',
126
+ )
127
+ .option('--dry-run', 'show the selection without writing')
128
+ .action(async (options) => {
129
+ const { runInit } = await import('./init.mjs');
130
+ await runInit(options);
131
+ });
132
+
133
+ program
134
+ .command('registry')
135
+ .description('Inspect registry.json.')
136
+ .option('--check', 'validate the registry')
137
+ .option('--list', 'print families and gates')
138
+ .action((options) => {
139
+ if (options.check) return registryCheck();
140
+ if (options.list) return registryList();
141
+ return fail('registry needs --check or --list');
142
+ });
143
+
144
+ program
145
+ .command('smoke')
146
+ .description(
147
+ 'Feed each gate a known violation and confirm it actually reacts (deny/warn). ' +
148
+ 'Catches a gate that is wired but silently allows. Exits non-zero on any no-reaction.',
149
+ )
150
+ .action(() => smokeGates());
151
+
152
+ registerTaskCommand(program);
153
+
154
+ program.parseAsync(process.argv).catch((error) => fail(error.message));