@devrik-tools/claude-gates 0.1.2 → 0.1.3

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 (104) hide show
  1. package/.claude-plugin/marketplace.json +23 -17
  2. package/cli/__tests__/config.test.mjs +113 -101
  3. package/cli/__tests__/install.test.mjs +102 -0
  4. package/cli/__tests__/materialize.test.mjs +95 -0
  5. package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
  6. package/cli/__tests__/task.test.mjs +124 -0
  7. package/cli/config.mjs +124 -113
  8. package/cli/index.mjs +100 -97
  9. package/cli/init.mjs +259 -256
  10. package/cli/install.mjs +150 -80
  11. package/cli/materialize.mjs +102 -51
  12. package/cli/registry.mjs +136 -127
  13. package/cli/task.mjs +140 -0
  14. package/package.json +74 -73
  15. package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
  16. package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
  17. package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
  18. package/plugins/gates/hooks/ask-adoption.mjs +147 -0
  19. package/plugins/gates/hooks/doctor.mjs +207 -0
  20. package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
  21. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
  22. package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
  23. package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
  24. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
  25. package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
  26. package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
  27. package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
  28. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
  29. package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
  30. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
  31. package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
  32. package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
  33. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
  34. package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
  35. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
  36. package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
  37. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
  38. package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
  39. package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
  40. package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
  41. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
  42. package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
  43. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
  44. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
  45. package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
  46. package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
  47. package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
  48. package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
  49. package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
  50. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
  51. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
  52. package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
  53. package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
  54. package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
  55. package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
  56. package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
  57. package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
  58. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
  59. package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
  60. package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
  61. package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
  62. package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
  63. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
  64. package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
  65. package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
  66. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
  67. package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
  68. package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
  69. package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
  70. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
  71. package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
  72. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
  73. package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
  74. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
  75. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
  76. package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
  77. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
  78. package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
  79. package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
  80. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
  81. package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
  82. package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
  83. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
  84. package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
  85. package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
  86. package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
  87. package/plugins/gates/hooks/hooks.json +346 -286
  88. package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
  89. package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
  90. package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
  91. package/plugins/gates/hooks/lib/config.mjs +172 -165
  92. package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
  93. package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
  94. package/plugins/gates/hooks/lib/signals.mjs +127 -0
  95. package/plugins/gates/hooks/wiring-check.mjs +227 -0
  96. package/plugins/tasks/.claude-plugin/plugin.json +9 -0
  97. package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
  98. package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
  99. package/plugins/tasks/hooks/hooks.json +26 -0
  100. package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
  101. package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
  102. package/plugins/tasks/hooks/register-requests.mjs +141 -108
  103. package/plugins/tasks/hooks/session-tasks.mjs +108 -0
  104. package/registry.json +793 -686
@@ -1,154 +1,173 @@
1
- import assert from 'node:assert/strict';
2
- import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { test } from 'node:test';
6
- import { isGateEnabled, gateParameters } from '../config.mjs';
7
-
8
- const GATE_KEY = 'blockDestructiveShellCommands';
9
-
10
- function temporaryDirectory() {
11
- return mkdtempSync(join(tmpdir(), 'claude-gates-config-'));
12
- }
13
-
14
- /** A project directory with a .git marker so config.mjs finds a project root. */
15
- function projectDirectory() {
16
- const root = temporaryDirectory();
17
- mkdirSync(join(root, '.git'));
18
- return root;
19
- }
20
-
21
- function writeProjectConfig(root, config) {
22
- mkdirSync(join(root, '.ai'), { recursive: true });
23
- writeFileSync(join(root, '.ai', 'config.json'), JSON.stringify(config));
24
- }
25
-
26
- function writeGlobalConfig(home, config) {
27
- const directory = join(home, '.claude', 'claude-gates');
28
- mkdirSync(directory, { recursive: true });
29
- writeFileSync(join(directory, 'config.json'), JSON.stringify(config));
30
- }
31
-
32
- test('isGateEnabled falls back to the registry default when no config exists anywhere', () => {
33
- const project = projectDirectory();
34
- const home = temporaryDirectory();
35
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
36
- assert.equal(isGateEnabled(GATE_KEY, false, project, { home }), false);
37
- });
38
-
39
- test('isGateEnabled reads a bool shorthand entry from the project config', () => {
40
- const project = projectDirectory();
41
- const home = temporaryDirectory();
42
- writeProjectConfig(project, { gates: { [GATE_KEY]: false } });
43
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
44
-
45
- const projectTrue = projectDirectory();
46
- writeProjectConfig(projectTrue, { gates: { [GATE_KEY]: true } });
47
- assert.equal(isGateEnabled(GATE_KEY, false, projectTrue, { home }), true);
48
- });
49
-
50
- test('isGateEnabled reads an object entry with an explicit enabled flag', () => {
51
- const project = projectDirectory();
52
- const home = temporaryDirectory();
53
- writeProjectConfig(project, {
54
- gates: { [GATE_KEY]: { enabled: false, someParam: 'x' } },
55
- });
56
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
57
- });
58
-
59
- test('isGateEnabled treats an object entry with no enabled field as silent (uses registry default)', () => {
60
- const project = projectDirectory();
61
- const home = temporaryDirectory();
62
- writeProjectConfig(project, {
63
- gates: { [GATE_KEY]: { someParam: 'x' } },
64
- });
65
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
66
- assert.equal(isGateEnabled(GATE_KEY, false, project, { home }), false);
67
- });
68
-
69
- test('isGateEnabled falls back to the global config when the project declares nothing', () => {
70
- const project = projectDirectory();
71
- const home = temporaryDirectory();
72
- writeGlobalConfig(home, { gates: { [GATE_KEY]: false } });
73
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
74
- });
75
-
76
- test('isGateEnabled: an existing project config wins over the global config even if silent about this gate', () => {
77
- const project = projectDirectory();
78
- const home = temporaryDirectory();
79
- writeGlobalConfig(home, { gates: { [GATE_KEY]: false } });
80
- writeProjectConfig(project, { gates: {} });
81
- // Project config exists (even without this gate declared): registry default wins,
82
- // global's false must NOT leak through.
83
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
84
- });
85
-
86
- test('isGateEnabled: corrupt project config is treated as absent and falls through to global', () => {
87
- const project = projectDirectory();
88
- const home = temporaryDirectory();
89
- mkdirSync(join(project, '.ai'), { recursive: true });
90
- writeFileSync(join(project, '.ai', 'config.json'), '{not valid json');
91
- writeGlobalConfig(home, { gates: { [GATE_KEY]: false } });
92
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
93
- });
94
-
95
- test('isGateEnabled: a malformed entry (string/number) is ignored, falls back to registry default', () => {
96
- const project = projectDirectory();
97
- const home = temporaryDirectory();
98
- writeProjectConfig(project, { gates: { [GATE_KEY]: 'yes' } });
99
- assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
100
- assert.equal(isGateEnabled(GATE_KEY, false, project, { home }), false);
101
-
102
- const projectNumber = projectDirectory();
103
- writeProjectConfig(projectNumber, { gates: { [GATE_KEY]: 1 } });
104
- assert.equal(isGateEnabled(GATE_KEY, false, projectNumber, { home }), false);
105
- });
106
-
107
- test('gateParameters returns {} when nothing is declared for the gate', () => {
108
- const project = projectDirectory();
109
- const home = temporaryDirectory();
110
- writeProjectConfig(project, { gates: {} });
111
- assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {});
112
- });
113
-
114
- test('gateParameters returns {} for a bool shorthand entry (no params, just enabled)', () => {
115
- const project = projectDirectory();
116
- const home = temporaryDirectory();
117
- writeProjectConfig(project, { gates: { [GATE_KEY]: true } });
118
- assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {});
119
- });
120
-
121
- test('gateParameters strips enabled and returns the remaining params from the project config', () => {
122
- const project = projectDirectory();
123
- const home = temporaryDirectory();
124
- writeProjectConfig(project, {
125
- gates: {
126
- [GATE_KEY]: { enabled: true, protectedPaths: ['a', 'b'] },
127
- },
128
- });
129
- assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {
130
- protectedPaths: ['a', 'b'],
131
- });
132
- });
133
-
134
- test('gateParameters: project params replace (do not merge with) global params', () => {
135
- const project = projectDirectory();
136
- const home = temporaryDirectory();
137
- writeGlobalConfig(home, {
138
- gates: {
139
- [GATE_KEY]: { protectedPaths: ['global-a'], extra: 'g' },
140
- },
141
- });
142
- writeProjectConfig(project, {
143
- gates: { [GATE_KEY]: { protectedPaths: ['project-a'] } },
144
- });
145
- assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {
146
- protectedPaths: ['project-a'],
147
- });
148
- });
149
-
150
- test('gateParameters returns {} when no config exists anywhere', () => {
151
- const project = projectDirectory();
152
- const home = temporaryDirectory();
153
- assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {});
154
- });
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { test } from 'node:test';
6
+ import { isGateEnabled, gateParameters } from '../config.mjs';
7
+
8
+ const GATE_KEY = 'blockDestructiveShellCommands';
9
+
10
+ function temporaryDirectory() {
11
+ return mkdtempSync(join(tmpdir(), 'claude-gates-config-'));
12
+ }
13
+
14
+ /** A project directory with a .git marker so config.mjs finds a project root. */
15
+ function projectDirectory() {
16
+ const root = temporaryDirectory();
17
+ mkdirSync(join(root, '.git'));
18
+ return root;
19
+ }
20
+
21
+ function writeProjectConfig(root, config) {
22
+ mkdirSync(join(root, '.ai'), { recursive: true });
23
+ writeFileSync(join(root, '.ai', 'config.json'), JSON.stringify(config));
24
+ }
25
+
26
+ function writeGlobalConfig(home, config) {
27
+ const directory = join(home, '.claude', 'claude-gates');
28
+ mkdirSync(directory, { recursive: true });
29
+ writeFileSync(join(directory, 'config.json'), JSON.stringify(config));
30
+ }
31
+
32
+ test('isGateEnabled falls back to the registry default when no config exists anywhere', () => {
33
+ const project = projectDirectory();
34
+ const home = temporaryDirectory();
35
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
36
+ assert.equal(isGateEnabled(GATE_KEY, false, project, { home }), false);
37
+ });
38
+
39
+ test('isGateEnabled reads a bool shorthand entry from the project config', () => {
40
+ const project = projectDirectory();
41
+ const home = temporaryDirectory();
42
+ writeProjectConfig(project, { gates: { [GATE_KEY]: false } });
43
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
44
+
45
+ const projectTrue = projectDirectory();
46
+ writeProjectConfig(projectTrue, { gates: { [GATE_KEY]: true } });
47
+ assert.equal(isGateEnabled(GATE_KEY, false, projectTrue, { home }), true);
48
+ });
49
+
50
+ test('isGateEnabled reads an object entry with an explicit enabled flag', () => {
51
+ const project = projectDirectory();
52
+ const home = temporaryDirectory();
53
+ writeProjectConfig(project, {
54
+ gates: { [GATE_KEY]: { enabled: false, someParam: 'x' } },
55
+ });
56
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
57
+ });
58
+
59
+ test('isGateEnabled treats an object entry with no enabled field as silent (uses registry default)', () => {
60
+ const project = projectDirectory();
61
+ const home = temporaryDirectory();
62
+ writeProjectConfig(project, {
63
+ gates: { [GATE_KEY]: { someParam: 'x' } },
64
+ });
65
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
66
+ assert.equal(isGateEnabled(GATE_KEY, false, project, { home }), false);
67
+ });
68
+
69
+ test('isGateEnabled falls back to the global config when the project declares nothing', () => {
70
+ const project = projectDirectory();
71
+ const home = temporaryDirectory();
72
+ writeGlobalConfig(home, { gates: { [GATE_KEY]: false } });
73
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
74
+ });
75
+
76
+ test('isGateEnabled: an existing project config wins over the global config even if silent about this gate', () => {
77
+ const project = projectDirectory();
78
+ const home = temporaryDirectory();
79
+ writeGlobalConfig(home, { gates: { [GATE_KEY]: false } });
80
+ writeProjectConfig(project, { gates: {} });
81
+ // Project config exists (even without this gate declared): registry default wins,
82
+ // global's false must NOT leak through.
83
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
84
+ });
85
+
86
+ test('isGateEnabled: corrupt project config is treated as absent and falls through to global', () => {
87
+ const project = projectDirectory();
88
+ const home = temporaryDirectory();
89
+ mkdirSync(join(project, '.ai'), { recursive: true });
90
+ writeFileSync(join(project, '.ai', 'config.json'), '{not valid json');
91
+ writeGlobalConfig(home, { gates: { [GATE_KEY]: false } });
92
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), false);
93
+ });
94
+
95
+ test('isGateEnabled: a malformed entry (string/number) is ignored, falls back to registry default', () => {
96
+ const project = projectDirectory();
97
+ const home = temporaryDirectory();
98
+ writeProjectConfig(project, { gates: { [GATE_KEY]: 'yes' } });
99
+ assert.equal(isGateEnabled(GATE_KEY, true, project, { home }), true);
100
+ assert.equal(isGateEnabled(GATE_KEY, false, project, { home }), false);
101
+
102
+ const projectNumber = projectDirectory();
103
+ writeProjectConfig(projectNumber, { gates: { [GATE_KEY]: 1 } });
104
+ assert.equal(isGateEnabled(GATE_KEY, false, projectNumber, { home }), false);
105
+ });
106
+
107
+ test('gateParameters returns {} when nothing is declared for the gate', () => {
108
+ const project = projectDirectory();
109
+ const home = temporaryDirectory();
110
+ writeProjectConfig(project, { gates: {} });
111
+ assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {});
112
+ });
113
+
114
+ test('gateParameters returns {} for a bool shorthand entry (no params, just enabled)', () => {
115
+ const project = projectDirectory();
116
+ const home = temporaryDirectory();
117
+ writeProjectConfig(project, { gates: { [GATE_KEY]: true } });
118
+ assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {});
119
+ });
120
+
121
+ test('gateParameters strips enabled and returns the remaining params from the project config', () => {
122
+ const project = projectDirectory();
123
+ const home = temporaryDirectory();
124
+ writeProjectConfig(project, {
125
+ gates: {
126
+ [GATE_KEY]: { enabled: true, protectedPaths: ['a', 'b'] },
127
+ },
128
+ });
129
+ assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {
130
+ protectedPaths: ['a', 'b'],
131
+ });
132
+ });
133
+
134
+ test('gateParameters: project params replace (do not merge with) global params', () => {
135
+ const project = projectDirectory();
136
+ const home = temporaryDirectory();
137
+ writeGlobalConfig(home, {
138
+ gates: {
139
+ [GATE_KEY]: { protectedPaths: ['global-a'], extra: 'g' },
140
+ },
141
+ });
142
+ writeProjectConfig(project, {
143
+ gates: { [GATE_KEY]: { protectedPaths: ['project-a'] } },
144
+ });
145
+ assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {
146
+ protectedPaths: ['project-a'],
147
+ });
148
+ });
149
+
150
+ test('gateParameters returns {} when no config exists anywhere', () => {
151
+ const project = projectDirectory();
152
+ const home = temporaryDirectory();
153
+ assert.deepEqual(gateParameters(GATE_KEY, project, { home }), {});
154
+ });
155
+
156
+ test('a project config saved WITH a UTF-8 BOM is still read (not silently dropped)', () => {
157
+ // A BOM is what a Windows editor or PowerShell 5.1 `Set-Content -Encoding utf8` prepends.
158
+ // Before the fix, JSON.parse threw on the leading BOM, the project config was treated as
159
+ // absent, and its gate disables were silently ignored (falling through to global/registry).
160
+ // Here the project disables the gate; the BOM must not resurrect it.
161
+ const project = projectDirectory();
162
+ const home = temporaryDirectory();
163
+ mkdirSync(join(project, '.ai'), { recursive: true });
164
+ writeFileSync(
165
+ join(project, '.ai', 'config.json'),
166
+ '' + JSON.stringify({ gates: { [GATE_KEY]: { enabled: false } } }),
167
+ );
168
+ assert.equal(
169
+ isGateEnabled(GATE_KEY, true, project, { home }),
170
+ false,
171
+ 'the BOM-prefixed project disable must take effect',
172
+ );
173
+ });