@devrik-tools/claude-gates 0.1.1 → 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 (105) 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 -0
  25. package/plugins/gates/hooks/gates/autonomous-mode/test.mjs +58 -0
  26. package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
  27. package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
  28. package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
  29. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
  30. package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
  31. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
  32. package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
  33. package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
  34. package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
  35. package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
  36. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
  37. package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
  38. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
  39. package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
  40. package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
  41. package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
  42. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
  43. package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
  44. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
  45. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
  46. package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
  47. package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
  48. package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
  49. package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
  50. package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
  51. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
  52. package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
  53. package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
  54. package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
  55. package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
  56. package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
  57. package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
  58. package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
  59. package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
  60. package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
  61. package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
  62. package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
  63. package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
  64. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
  65. package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
  66. package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
  67. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
  68. package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
  69. package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
  70. package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
  71. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
  72. package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
  73. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
  74. package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
  75. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
  76. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
  77. package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
  78. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
  79. package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
  80. package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
  81. package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
  82. package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
  83. package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
  84. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
  85. package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
  86. package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
  87. package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
  88. package/plugins/gates/hooks/hooks.json +346 -266
  89. package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
  90. package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
  91. package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
  92. package/plugins/gates/hooks/lib/config.mjs +172 -165
  93. package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
  94. package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
  95. package/plugins/gates/hooks/lib/signals.mjs +127 -0
  96. package/plugins/gates/hooks/wiring-check.mjs +227 -0
  97. package/plugins/tasks/.claude-plugin/plugin.json +9 -0
  98. package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
  99. package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
  100. package/plugins/tasks/hooks/hooks.json +26 -0
  101. package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
  102. package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
  103. package/plugins/tasks/hooks/register-requests.mjs +141 -108
  104. package/plugins/tasks/hooks/session-tasks.mjs +108 -0
  105. package/registry.json +793 -668
@@ -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
+ });