@devrik-tools/claude-gates 0.4.0 → 0.7.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 +39 -4
- package/README.md +34 -5
- package/cli/config.mjs +126 -124
- package/cli/init.mjs +303 -276
- package/cli/install.mjs +281 -175
- package/cli/materialize.mjs +103 -102
- package/cli/registry.mjs +139 -136
- package/cli/smoke-fixtures.json +65 -0
- package/cli/task.mjs +140 -140
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/ask-adoption.mjs +147 -147
- package/plugins/gates/hooks/doctor.mjs +207 -207
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
- package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
- package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
- package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
- package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
- package/plugins/gates/hooks/hooks.json +61 -0
- package/plugins/gates/hooks/lib/config.mjs +179 -172
- package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
- package/plugins/gates/hooks/lib/signals.mjs +172 -127
- package/plugins/gates/hooks/wiring-check.mjs +227 -227
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/hooks.json +26 -26
- package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
- package/plugins/tasks/hooks/register-requests.mjs +145 -145
- package/plugins/tasks/hooks/session-tasks.mjs +108 -108
- package/registry.json +192 -1
|
@@ -1,131 +1,211 @@
|
|
|
1
|
-
// root-whitelist — denies creating a new file or folder at the project's ROOT unless it
|
|
2
|
-
// is on the declared whitelist. Migrated from ~/.claude/hooks/guard-root-whitelist.mjs.
|
|
3
|
-
// A root grows orphaned files silently: nothing else stops a stray file from landing next
|
|
4
|
-
// to package.json. Only the top level of the project is governed — anything nested is
|
|
5
|
-
// this gate's business only insofar as its first path segment is the new thing at the root.
|
|
6
|
-
//
|
|
7
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
8
|
-
// rootFilesWhitelist root-level file names allowed to be created. Replaces the
|
|
9
|
-
// built-in list wholesale.
|
|
10
|
-
// rootFoldersWhitelist root-level folder names allowed to be created. Replaces the
|
|
11
|
-
// built-in list wholesale.
|
|
12
|
-
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
13
|
-
// its override replaces. A dotfile/dotfolder (.gitignore, .claude) is always allowed —
|
|
14
|
-
// dotfiles are a different, well-known convention this gate does not police.
|
|
15
|
-
|
|
16
|
-
import { basename, isAbsolute, resolve, sep } from 'node:path';
|
|
17
|
-
import {
|
|
18
|
-
runGate,
|
|
19
|
-
deny,
|
|
20
|
-
toolInGroups,
|
|
21
|
-
writtenPathOf,
|
|
22
|
-
shellWrittenPaths,
|
|
23
|
-
} from '../../lib/hook-io.mjs';
|
|
24
|
-
|
|
25
|
-
const GATE_ID = 'root-whitelist';
|
|
26
|
-
const CONFIG_KEY = 'blockPathsOutsideRootWhitelist';
|
|
27
|
-
|
|
28
|
-
const DEFAULT_ROOT_FILES_WHITELIST = [
|
|
29
|
-
'AGENTS.md',
|
|
30
|
-
'CLAUDE.md',
|
|
31
|
-
'README.md',
|
|
32
|
-
'LICENSE',
|
|
33
|
-
'package.json',
|
|
34
|
-
'package-lock.json',
|
|
35
|
-
'tsconfig.json',
|
|
36
|
-
'eslint.config.mjs',
|
|
37
|
-
'eslint.config.js',
|
|
38
|
-
'eslint.config.cjs',
|
|
39
|
-
'.prettierrc',
|
|
40
|
-
'.prettierrc.json',
|
|
41
|
-
'.prettierignore',
|
|
42
|
-
'.eslintignore',
|
|
43
|
-
'cspell.json',
|
|
44
|
-
'.cspell.json',
|
|
45
|
-
'.gitignore',
|
|
46
|
-
'.env',
|
|
47
|
-
'.env.example',
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1
|
+
// root-whitelist — denies creating a new file or folder at the project's ROOT unless it
|
|
2
|
+
// is on the declared whitelist. Migrated from ~/.claude/hooks/guard-root-whitelist.mjs.
|
|
3
|
+
// A root grows orphaned files silently: nothing else stops a stray file from landing next
|
|
4
|
+
// to package.json. Only the top level of the project is governed — anything nested is
|
|
5
|
+
// this gate's business only insofar as its first path segment is the new thing at the root.
|
|
6
|
+
//
|
|
7
|
+
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
8
|
+
// rootFilesWhitelist root-level file names allowed to be created. Replaces the
|
|
9
|
+
// built-in list wholesale.
|
|
10
|
+
// rootFoldersWhitelist root-level folder names allowed to be created. Replaces the
|
|
11
|
+
// built-in list wholesale.
|
|
12
|
+
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
13
|
+
// its override replaces. A dotfile/dotfolder (.gitignore, .claude) is always allowed —
|
|
14
|
+
// dotfiles are a different, well-known convention this gate does not police.
|
|
15
|
+
|
|
16
|
+
import { basename, isAbsolute, resolve, sep } from 'node:path';
|
|
17
|
+
import {
|
|
18
|
+
runGate,
|
|
19
|
+
deny,
|
|
20
|
+
toolInGroups,
|
|
21
|
+
writtenPathOf,
|
|
22
|
+
shellWrittenPaths,
|
|
23
|
+
} from '../../lib/hook-io.mjs';
|
|
24
|
+
|
|
25
|
+
const GATE_ID = 'root-whitelist';
|
|
26
|
+
const CONFIG_KEY = 'blockPathsOutsideRootWhitelist';
|
|
27
|
+
|
|
28
|
+
const DEFAULT_ROOT_FILES_WHITELIST = [
|
|
29
|
+
'AGENTS.md',
|
|
30
|
+
'CLAUDE.md',
|
|
31
|
+
'README.md',
|
|
32
|
+
'LICENSE',
|
|
33
|
+
'package.json',
|
|
34
|
+
'package-lock.json',
|
|
35
|
+
'tsconfig.json',
|
|
36
|
+
'eslint.config.mjs',
|
|
37
|
+
'eslint.config.js',
|
|
38
|
+
'eslint.config.cjs',
|
|
39
|
+
'.prettierrc',
|
|
40
|
+
'.prettierrc.json',
|
|
41
|
+
'.prettierignore',
|
|
42
|
+
'.eslintignore',
|
|
43
|
+
'cspell.json',
|
|
44
|
+
'.cspell.json',
|
|
45
|
+
'.gitignore',
|
|
46
|
+
'.env',
|
|
47
|
+
'.env.example',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Root folders that real projects legitimately create. The old list (src/tests/docs/
|
|
51
|
+
// scripts/plugins) was calibrated to THIS repo and wrongly blocked the root folders every
|
|
52
|
+
// common framework creates — app (Next/Nuxt/Laravel/Expo), lib, public, dist, components,
|
|
53
|
+
// pages, api, and so on. A root-level folder is a deliberate, structural choice; the value
|
|
54
|
+
// of this gate is catching a stray FILE dropped next to package.json, not second-guessing a
|
|
55
|
+
// project's directory layout. So the folder whitelist is broad (real framework/tooling
|
|
56
|
+
// conventions) while the file whitelist stays the strict part. A project still overrides
|
|
57
|
+
// either list wholesale via config.
|
|
58
|
+
const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
59
|
+
// source / app code
|
|
60
|
+
'src',
|
|
61
|
+
'app',
|
|
62
|
+
'lib',
|
|
63
|
+
'pkg',
|
|
64
|
+
'packages',
|
|
65
|
+
'apps',
|
|
66
|
+
'libs',
|
|
67
|
+
'components',
|
|
68
|
+
'pages',
|
|
69
|
+
'api',
|
|
70
|
+
'routes',
|
|
71
|
+
'server',
|
|
72
|
+
'client',
|
|
73
|
+
'shared',
|
|
74
|
+
'core',
|
|
75
|
+
'modules',
|
|
76
|
+
'features',
|
|
77
|
+
// web / framework conventions
|
|
78
|
+
'public',
|
|
79
|
+
'static',
|
|
80
|
+
'assets',
|
|
81
|
+
'styles',
|
|
82
|
+
'templates',
|
|
83
|
+
'views',
|
|
84
|
+
'layouts',
|
|
85
|
+
'middleware',
|
|
86
|
+
'plugins',
|
|
87
|
+
'store',
|
|
88
|
+
'stores',
|
|
89
|
+
'hooks',
|
|
90
|
+
'composables',
|
|
91
|
+
'utils',
|
|
92
|
+
'helpers',
|
|
93
|
+
'services',
|
|
94
|
+
'config',
|
|
95
|
+
'configs',
|
|
96
|
+
'i18n',
|
|
97
|
+
'locales',
|
|
98
|
+
// tests / docs / tooling
|
|
99
|
+
'tests',
|
|
100
|
+
'test',
|
|
101
|
+
'__tests__',
|
|
102
|
+
'e2e',
|
|
103
|
+
'cypress',
|
|
104
|
+
'docs',
|
|
105
|
+
'doc',
|
|
106
|
+
'examples',
|
|
107
|
+
'example',
|
|
108
|
+
'scripts',
|
|
109
|
+
'tools',
|
|
110
|
+
'bin',
|
|
111
|
+
// build / generated (present in many repos, not worth blocking)
|
|
112
|
+
'dist',
|
|
113
|
+
'build',
|
|
114
|
+
'out',
|
|
115
|
+
'coverage',
|
|
116
|
+
'node_modules',
|
|
117
|
+
'vendor',
|
|
118
|
+
'target',
|
|
119
|
+
// infra / ops
|
|
120
|
+
'migrations',
|
|
121
|
+
'prisma',
|
|
122
|
+
'db',
|
|
123
|
+
'database',
|
|
124
|
+
'docker',
|
|
125
|
+
'.github',
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// The whitelist verdict for one target path. Returns a deny reason string when the path is a
|
|
129
|
+
// non-whitelisted new thing at the project root, or null when it is allowed (outside the root,
|
|
130
|
+
// a dotfile, or whitelisted). Shared by the write-tool path and every shell-created path, so a
|
|
131
|
+
// `printf x > basura.txt` is judged by the same rule as a Write to basura.txt.
|
|
132
|
+
function rootWhitelistViolation(target, filesWhitelist, foldersWhitelist) {
|
|
133
|
+
if (!target) return null;
|
|
134
|
+
|
|
135
|
+
// Some tools send a path relative to cwd rather than absolute. resolve() leaves an
|
|
136
|
+
// already-absolute path merely normalized ('..' collapsed, separators fixed), and resolves
|
|
137
|
+
// a relative one against process.cwd() — so either shape lands on the same absolute path.
|
|
138
|
+
const normalized = isAbsolute(target)
|
|
139
|
+
? resolve(target)
|
|
140
|
+
: resolve(process.cwd(), target);
|
|
141
|
+
const projectRoot = process.cwd() + sep;
|
|
142
|
+
|
|
143
|
+
// A path outside the project (scratchpad, temp, another repo) is not at its root — not this
|
|
144
|
+
// rule's business. Judged AFTER resolving, so a relative path is judged by where it lands.
|
|
145
|
+
if (!normalized.startsWith(projectRoot)) return null;
|
|
146
|
+
|
|
147
|
+
const relativePath = normalized.slice(projectRoot.length);
|
|
148
|
+
|
|
149
|
+
if (!relativePath.includes(sep)) {
|
|
150
|
+
const fileName = basename(relativePath);
|
|
151
|
+
if (fileName.startsWith('.') || filesWhitelist.has(fileName.toLowerCase()))
|
|
152
|
+
return null;
|
|
153
|
+
return `'${fileName}' at the project root is not on the whitelist (${[...filesWhitelist].join(', ')}).`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const topDirectory = relativePath.split(sep)[0];
|
|
157
|
+
if (
|
|
158
|
+
topDirectory.startsWith('.') ||
|
|
159
|
+
foldersWhitelist.has(topDirectory.toLowerCase())
|
|
160
|
+
) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return `Folder '${topDirectory}' at the project root is not on the whitelist (${[...foldersWhitelist].join(', ')}).`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
runGate(
|
|
167
|
+
{
|
|
168
|
+
id: GATE_ID,
|
|
169
|
+
configKey: CONFIG_KEY,
|
|
170
|
+
enabledByDefault: true,
|
|
171
|
+
defaultParams: {
|
|
172
|
+
rootFilesWhitelist: DEFAULT_ROOT_FILES_WHITELIST,
|
|
173
|
+
rootFoldersWhitelist: DEFAULT_ROOT_FOLDERS_WHITELIST,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
({ toolName, toolInput, parameters }) => {
|
|
177
|
+
const isWrite = toolInGroups(toolName, ['write']);
|
|
178
|
+
const isShell = toolInGroups(toolName, ['shell']);
|
|
179
|
+
if (!isWrite && !isShell) return;
|
|
180
|
+
|
|
181
|
+
// Windows filesystems are case-insensitive: 'Package.json' and 'package.json' are the same
|
|
182
|
+
// file. Comparing lowercase on both sides avoids a false positive on a case difference.
|
|
183
|
+
const filesWhitelist = new Set(
|
|
184
|
+
(parameters.rootFilesWhitelist ?? []).map((name) => name.toLowerCase()),
|
|
185
|
+
);
|
|
186
|
+
const foldersWhitelist = new Set(
|
|
187
|
+
(parameters.rootFoldersWhitelist ?? []).map((name) => name.toLowerCase()),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// The paths to judge: a write tool's target, OR every path a shell command creates via a
|
|
191
|
+
// redirection / touch / tee / cp / mv. The latter closes the hole where `> basura.txt`
|
|
192
|
+
// created a root file that a Write to the same name would have been denied. This does not
|
|
193
|
+
// resolve a dynamically built path (`> "$f"`) — that honest limitation is the boundary of
|
|
194
|
+
// a regex, and is why the write-tool path (which carries a concrete file_path) stays the
|
|
195
|
+
// primary, most reliable surface.
|
|
196
|
+
const targets = isShell
|
|
197
|
+
? shellWrittenPaths(
|
|
198
|
+
String(toolInput?.command ?? toolInput?.CommandLine ?? ''),
|
|
199
|
+
)
|
|
200
|
+
: [writtenPathOf(toolInput)];
|
|
201
|
+
|
|
202
|
+
for (const target of targets) {
|
|
203
|
+
const reason = rootWhitelistViolation(
|
|
204
|
+
target,
|
|
205
|
+
filesWhitelist,
|
|
206
|
+
foldersWhitelist,
|
|
207
|
+
);
|
|
208
|
+
if (reason) deny(GATE_ID, reason);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
);
|