@devrik-tools/claude-gates 0.7.2 → 0.9.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 +3 -3
- package/README.es.md +129 -11
- package/README.md +120 -10
- 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 +12 -1
- package/cli/selection.mjs +18 -0
- package/cli/smoke-fixtures.json +159 -25
- package/cli/task.mjs +104 -11
- package/cli/toggle.mjs +125 -0
- package/package.json +4 -3
- 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 +94 -215
- package/plugins/gates/hooks/gates/circuit-breaker/track.mjs +285 -0
- 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 +53 -95
- 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 +338 -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/no-trivial-scripts/index.mjs +114 -0
- 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/require-monitor/index.mjs +126 -0
- package/plugins/gates/hooks/gates/require-task-split/index.mjs +88 -0
- 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 +152 -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 +451 -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 +148 -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 +12 -6
- package/plugins/tasks/hooks/register-requests.mjs +37 -10
- package/registry.json +249 -38
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
// root-whitelist — denies
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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';
|
|
1
|
+
// root-whitelist — denies CREATING a file or folder at the project's root unless its name is
|
|
2
|
+
// whitelisted. Only the top level is governed, and only new entries: an edit to a file that
|
|
3
|
+
// already exists (registry.json, CHANGELOG.md) is not this gate's business, nor is a path
|
|
4
|
+
// outside the project (a scratchpad, /tmp). Dotfiles and dotfolders are always allowed — a
|
|
5
|
+
// different, well-known convention. The root is the nearest .git/.ai ancestor of the cwd, so a
|
|
6
|
+
// tool call from a subfolder is judged against the same root. A path built from a variable is
|
|
7
|
+
// not resolved.
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
11
|
+
import { projectRootOf } from '../../lib/config.mjs';
|
|
17
12
|
import {
|
|
18
|
-
runGate,
|
|
19
13
|
deny,
|
|
14
|
+
runGate,
|
|
15
|
+
shellCommandOf,
|
|
16
|
+
shellWrittenPaths,
|
|
20
17
|
toolInGroups,
|
|
21
18
|
writtenPathOf,
|
|
22
|
-
shellWrittenPaths,
|
|
23
19
|
} from '../../lib/hook-io.mjs';
|
|
24
20
|
|
|
25
21
|
const GATE_ID = 'root-whitelist';
|
|
@@ -47,16 +43,9 @@ const DEFAULT_ROOT_FILES_WHITELIST = [
|
|
|
47
43
|
'.env.example',
|
|
48
44
|
];
|
|
49
45
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
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.
|
|
46
|
+
// Broad on purpose: a root folder is a deliberate structural choice every framework makes
|
|
47
|
+
// differently; the value of this gate is the stray FILE next to package.json.
|
|
58
48
|
const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
59
|
-
// source / app code
|
|
60
49
|
'src',
|
|
61
50
|
'app',
|
|
62
51
|
'lib',
|
|
@@ -74,7 +63,6 @@ const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
|
74
63
|
'core',
|
|
75
64
|
'modules',
|
|
76
65
|
'features',
|
|
77
|
-
// web / framework conventions
|
|
78
66
|
'public',
|
|
79
67
|
'static',
|
|
80
68
|
'assets',
|
|
@@ -95,7 +83,6 @@ const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
|
95
83
|
'configs',
|
|
96
84
|
'i18n',
|
|
97
85
|
'locales',
|
|
98
|
-
// tests / docs / tooling
|
|
99
86
|
'tests',
|
|
100
87
|
'test',
|
|
101
88
|
'__tests__',
|
|
@@ -108,7 +95,6 @@ const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
|
108
95
|
'scripts',
|
|
109
96
|
'tools',
|
|
110
97
|
'bin',
|
|
111
|
-
// build / generated (present in many repos, not worth blocking)
|
|
112
98
|
'dist',
|
|
113
99
|
'build',
|
|
114
100
|
'out',
|
|
@@ -116,7 +102,6 @@ const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
|
116
102
|
'node_modules',
|
|
117
103
|
'vendor',
|
|
118
104
|
'target',
|
|
119
|
-
// infra / ops
|
|
120
105
|
'migrations',
|
|
121
106
|
'prisma',
|
|
122
107
|
'db',
|
|
@@ -125,42 +110,133 @@ const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
|
125
110
|
'.github',
|
|
126
111
|
];
|
|
127
112
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
function
|
|
133
|
-
if (!
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
}
|
|
113
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
114
|
+
const GIT_BASH_DRIVE_PATTERN = /^\/([a-z])(?=\/|$)/i;
|
|
115
|
+
|
|
116
|
+
// Git Bash spells C:\Users as /c/Users; on Windows that form is otherwise resolved to C:\c\.
|
|
117
|
+
function toNativePath(path) {
|
|
118
|
+
if (!IS_WINDOWS) return path;
|
|
119
|
+
return path.replace(
|
|
120
|
+
GIT_BASH_DRIVE_PATTERN,
|
|
121
|
+
(_, drive) => `${drive.toUpperCase()}:`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
155
124
|
|
|
156
|
-
|
|
125
|
+
function comparable(path) {
|
|
126
|
+
return IS_WINDOWS ? path.toLowerCase() : path;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The first path segment under the root, or null when the path is outside the project.
|
|
130
|
+
function rootEntryOf(target, cwd, root) {
|
|
131
|
+
const absolute = resolve(cwd, toNativePath(target));
|
|
132
|
+
const relativePath = relative(comparable(root), comparable(absolute));
|
|
157
133
|
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
134
|
+
!relativePath ||
|
|
135
|
+
relativePath.startsWith('..') ||
|
|
136
|
+
isAbsolute(relativePath)
|
|
137
|
+
)
|
|
138
|
+
return null;
|
|
139
|
+
const [name] = relativePath.split(sep);
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
isNested: relativePath.includes(sep),
|
|
143
|
+
path: resolve(root, name),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function whitelistViolation({ target, isFolder }, context) {
|
|
148
|
+
const entry = rootEntryOf(target, context.cwd, context.root);
|
|
149
|
+
if (!entry || entry.name.startsWith('.') || existsSync(entry.path))
|
|
161
150
|
return null;
|
|
151
|
+
const judgedAsFolder = isFolder || entry.isNested;
|
|
152
|
+
const whitelist = judgedAsFolder
|
|
153
|
+
? context.foldersWhitelist
|
|
154
|
+
: context.filesWhitelist;
|
|
155
|
+
if (whitelist.has(entry.name.toLowerCase())) return null;
|
|
156
|
+
const kind = judgedAsFolder ? 'Folder' : 'File';
|
|
157
|
+
const parameter = judgedAsFolder
|
|
158
|
+
? 'rootFoldersWhitelist'
|
|
159
|
+
: 'rootFilesWhitelist';
|
|
160
|
+
return (
|
|
161
|
+
`${kind} '${entry.name}' would be created at the project root and is not on the ` +
|
|
162
|
+
`whitelist (${[...whitelist].join(', ')}). Put it inside an existing folder, or add ` +
|
|
163
|
+
`it to ${parameter} under ${CONFIG_KEY} in .ai/config.json.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Shell targets ───────────────────────────────────────────────────────────────────
|
|
168
|
+
const SEGMENT_SEPARATOR = /;|&&|\|\||\||\n/;
|
|
169
|
+
const ARGUMENT_PATTERN = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
170
|
+
const MKDIR_PREFIX = /^mkdir(?=\s)/i;
|
|
171
|
+
const GIT_CLONE_PREFIX = /^git\s+clone(?=\s)/i;
|
|
172
|
+
const NEW_ITEM_PREFIX = /^New-Item(?=\s)/i;
|
|
173
|
+
const DIRECTORY_ITEM_TYPE = /-ItemType\s+Directory\b/i;
|
|
174
|
+
const NAMED_PATH_PARAMETER =
|
|
175
|
+
/-(?:Path|Name|LiteralPath)\s+("[^"]*"|'[^']*'|\S+)/i;
|
|
176
|
+
|
|
177
|
+
function nonFlagArguments(text) {
|
|
178
|
+
const found = [];
|
|
179
|
+
for (const match of text.matchAll(ARGUMENT_PATTERN)) {
|
|
180
|
+
const token = match[1] ?? match[2] ?? match[3];
|
|
181
|
+
if (!token.startsWith('-')) found.push(token);
|
|
162
182
|
}
|
|
163
|
-
return
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function argumentsAfter(prefix, segment) {
|
|
187
|
+
const match = prefix.exec(segment);
|
|
188
|
+
return match ? segment.slice(match[0].length) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function newDirectoryItemTarget(segment) {
|
|
192
|
+
if (!NEW_ITEM_PREFIX.test(segment) || !DIRECTORY_ITEM_TYPE.test(segment))
|
|
193
|
+
return null;
|
|
194
|
+
const remainder = segment.replace(DIRECTORY_ITEM_TYPE, '');
|
|
195
|
+
const named = NAMED_PATH_PARAMETER.exec(remainder);
|
|
196
|
+
if (named) return named[1].replace(/^["']|["']$/g, '');
|
|
197
|
+
return (
|
|
198
|
+
nonFlagArguments(argumentsAfter(NEW_ITEM_PREFIX, remainder))[0] ?? null
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function cloneDestination(argumentText) {
|
|
203
|
+
const [url, destination] = nonFlagArguments(argumentText);
|
|
204
|
+
if (destination) return destination;
|
|
205
|
+
const base = String(url ?? '')
|
|
206
|
+
.split(/[\\/]/)
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.at(-1);
|
|
209
|
+
return base ? base.replace(/\.git$/i, '') : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function folderTargetsOf(command) {
|
|
213
|
+
const folders = new Set();
|
|
214
|
+
for (const rawSegment of String(command).split(SEGMENT_SEPARATOR)) {
|
|
215
|
+
const segment = rawSegment.trim();
|
|
216
|
+
const mkdirArguments = argumentsAfter(MKDIR_PREFIX, segment);
|
|
217
|
+
if (mkdirArguments !== null)
|
|
218
|
+
nonFlagArguments(mkdirArguments).forEach((path) => folders.add(path));
|
|
219
|
+
const directoryItem = newDirectoryItemTarget(segment);
|
|
220
|
+
if (directoryItem) folders.add(directoryItem);
|
|
221
|
+
const cloneArguments = argumentsAfter(GIT_CLONE_PREFIX, segment);
|
|
222
|
+
const destination =
|
|
223
|
+
cloneArguments === null ? null : cloneDestination(cloneArguments);
|
|
224
|
+
if (destination) folders.add(destination);
|
|
225
|
+
}
|
|
226
|
+
return folders;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function shellTargetsOf(command) {
|
|
230
|
+
const folders = folderTargetsOf(command);
|
|
231
|
+
const targets = shellWrittenPaths(command).map((path) => ({
|
|
232
|
+
target: path,
|
|
233
|
+
isFolder: folders.has(path) || /[\\/]$/.test(path),
|
|
234
|
+
}));
|
|
235
|
+
for (const folder of folders) {
|
|
236
|
+
if (!targets.some((entry) => entry.target === folder))
|
|
237
|
+
targets.push({ target: folder, isFolder: true });
|
|
238
|
+
}
|
|
239
|
+
return targets.filter((entry) => !/[$`%]/.test(entry.target));
|
|
164
240
|
}
|
|
165
241
|
|
|
166
242
|
runGate(
|
|
@@ -173,38 +249,27 @@ runGate(
|
|
|
173
249
|
rootFoldersWhitelist: DEFAULT_ROOT_FOLDERS_WHITELIST,
|
|
174
250
|
},
|
|
175
251
|
},
|
|
176
|
-
({ toolName, toolInput, parameters }) => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
);
|
|
252
|
+
({ toolName, toolInput, parameters, cwd }) => {
|
|
253
|
+
let targets;
|
|
254
|
+
if (toolInGroups(toolName, ['write'])) {
|
|
255
|
+
const path = writtenPathOf(toolInput);
|
|
256
|
+
targets = path ? [{ target: path, isFolder: false }] : [];
|
|
257
|
+
} else if (toolInGroups(toolName, ['shell'])) {
|
|
258
|
+
targets = shellTargetsOf(shellCommandOf(toolInput));
|
|
259
|
+
} else {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (targets.length === 0) return;
|
|
263
|
+
|
|
264
|
+
const lowered = (names) => new Set(names.map((name) => name.toLowerCase()));
|
|
265
|
+
const context = {
|
|
266
|
+
cwd,
|
|
267
|
+
root: projectRootOf(cwd) ?? cwd,
|
|
268
|
+
filesWhitelist: lowered(parameters.rootFilesWhitelist),
|
|
269
|
+
foldersWhitelist: lowered(parameters.rootFoldersWhitelist),
|
|
270
|
+
};
|
|
271
|
+
for (const entry of targets) {
|
|
272
|
+
const reason = whitelistViolation(entry, context);
|
|
208
273
|
if (reason) deny(CONFIG_KEY, reason);
|
|
209
274
|
}
|
|
210
275
|
},
|