@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,184 +1,181 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
existsSync,
|
|
5
|
-
readdirSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
unlinkSync,
|
|
9
|
-
} from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
import { join, relative } from 'node:path';
|
|
12
|
-
import { runGate, deny, toolInGroups } from '../../lib/hook-io.mjs';
|
|
13
|
-
|
|
14
|
-
const GATE_ID = 'rule-skill-autodiscovery';
|
|
15
|
-
const CONFIG_KEY = 'autodiscoverRulesAndSkills';
|
|
16
|
-
|
|
17
|
-
const DEFAULT_RULES_DIR = 'rules';
|
|
18
|
-
const DEFAULT_GATE_FILE_NAMES = [
|
|
19
|
-
'gate.mjs',
|
|
20
|
-
'rules.mjs',
|
|
21
|
-
'check.mjs',
|
|
22
|
-
'verify.mjs',
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
function isGateFile(fileName, gateFileNames) {
|
|
26
|
-
return gateFileNames.includes(fileName) || fileName.endsWith('.gate.mjs');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function findScriptsIn(scriptsDirectory, gateFileNames) {
|
|
30
|
-
if (!existsSync(scriptsDirectory)) return [];
|
|
31
|
-
let entries;
|
|
32
|
-
try {
|
|
33
|
-
entries = readdirSync(scriptsDirectory, { withFileTypes: true });
|
|
34
|
-
} catch {
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
return entries
|
|
38
|
-
.filter((entry) => entry.isFile())
|
|
39
|
-
.filter((entry) => /\.(mjs|js)$/.test(entry.name))
|
|
40
|
-
.filter((entry) => isGateFile(entry.name, gateFileNames))
|
|
41
|
-
.map((entry) => join(scriptsDirectory, entry.name));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function discoverScripts(projectRoot, rulesDirectoryName, gateFileNames) {
|
|
45
|
-
const scripts = [
|
|
46
|
-
...findScriptsIn(join(projectRoot, rulesDirectoryName), gateFileNames),
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const skillsRoot = join(projectRoot, '.claude', 'skills');
|
|
50
|
-
if (existsSync(skillsRoot)) {
|
|
51
|
-
let skillDirectories;
|
|
52
|
-
try {
|
|
53
|
-
skillDirectories = readdirSync(skillsRoot, {
|
|
54
|
-
withFileTypes: true,
|
|
55
|
-
}).filter((entry) => entry.isDirectory());
|
|
56
|
-
} catch {
|
|
57
|
-
skillDirectories = [];
|
|
58
|
-
}
|
|
59
|
-
for (const skillDirectory of skillDirectories) {
|
|
60
|
-
scripts.push(
|
|
61
|
-
...findScriptsIn(join(skillsRoot, skillDirectory.name), gateFileNames),
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return scripts;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Security-surface protection ─────────────────────────────────────────────────────
|
|
70
|
-
// A discovered script runs with full Node privileges (no sandbox is realistically
|
|
71
|
-
// enforceable via execFileSync alone). The one privilege it must NEVER have is the
|
|
72
|
-
// power to mutate the gate configuration that governs whether it (or any other gate)
|
|
73
|
-
// runs at all — otherwise a discovered script can disable e.g.
|
|
74
|
-
// `blockDestructiveShellCommands` in the same tool call it was meant to be gated by,
|
|
75
|
-
// before any later gate could react. This is enforced deterministically: snapshot a
|
|
76
|
-
// hash of both config files (project + global) before executing, and after every
|
|
77
|
-
// script runs, compare. Any change is reverted immediately and the call is denied —
|
|
78
|
-
// regardless of whether the script itself exited 0.
|
|
79
|
-
const PROJECT_CONFIG_RELATIVE_PATH = join('.ai', 'config.json');
|
|
80
|
-
|
|
81
|
-
function globalConfigPath() {
|
|
82
|
-
return join(homedir(), '.claude', 'claude-gates', 'config.json');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function securityConfigPaths(projectRoot) {
|
|
86
|
-
return [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
content
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
);
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join, relative } from 'node:path';
|
|
12
|
+
import { runGate, deny, toolInGroups } from '../../lib/hook-io.mjs';
|
|
13
|
+
|
|
14
|
+
const GATE_ID = 'rule-skill-autodiscovery';
|
|
15
|
+
const CONFIG_KEY = 'autodiscoverRulesAndSkills';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_RULES_DIR = 'rules';
|
|
18
|
+
const DEFAULT_GATE_FILE_NAMES = [
|
|
19
|
+
'gate.mjs',
|
|
20
|
+
'rules.mjs',
|
|
21
|
+
'check.mjs',
|
|
22
|
+
'verify.mjs',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function isGateFile(fileName, gateFileNames) {
|
|
26
|
+
return gateFileNames.includes(fileName) || fileName.endsWith('.gate.mjs');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findScriptsIn(scriptsDirectory, gateFileNames) {
|
|
30
|
+
if (!existsSync(scriptsDirectory)) return [];
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(scriptsDirectory, { withFileTypes: true });
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return entries
|
|
38
|
+
.filter((entry) => entry.isFile())
|
|
39
|
+
.filter((entry) => /\.(mjs|js)$/.test(entry.name))
|
|
40
|
+
.filter((entry) => isGateFile(entry.name, gateFileNames))
|
|
41
|
+
.map((entry) => join(scriptsDirectory, entry.name));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function discoverScripts(projectRoot, rulesDirectoryName, gateFileNames) {
|
|
45
|
+
const scripts = [
|
|
46
|
+
...findScriptsIn(join(projectRoot, rulesDirectoryName), gateFileNames),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const skillsRoot = join(projectRoot, '.claude', 'skills');
|
|
50
|
+
if (existsSync(skillsRoot)) {
|
|
51
|
+
let skillDirectories;
|
|
52
|
+
try {
|
|
53
|
+
skillDirectories = readdirSync(skillsRoot, {
|
|
54
|
+
withFileTypes: true,
|
|
55
|
+
}).filter((entry) => entry.isDirectory());
|
|
56
|
+
} catch {
|
|
57
|
+
skillDirectories = [];
|
|
58
|
+
}
|
|
59
|
+
for (const skillDirectory of skillDirectories) {
|
|
60
|
+
scripts.push(
|
|
61
|
+
...findScriptsIn(join(skillsRoot, skillDirectory.name), gateFileNames),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return scripts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Security-surface protection ─────────────────────────────────────────────────────
|
|
70
|
+
// A discovered script runs with full Node privileges (no sandbox is realistically
|
|
71
|
+
// enforceable via execFileSync alone). The one privilege it must NEVER have is the
|
|
72
|
+
// power to mutate the gate configuration that governs whether it (or any other gate)
|
|
73
|
+
// runs at all — otherwise a discovered script can disable e.g.
|
|
74
|
+
// `blockDestructiveShellCommands` in the same tool call it was meant to be gated by,
|
|
75
|
+
// before any later gate could react. This is enforced deterministically: snapshot a
|
|
76
|
+
// hash of both config files (project + global) before executing, and after every
|
|
77
|
+
// script runs, compare. Any change is reverted immediately and the call is denied —
|
|
78
|
+
// regardless of whether the script itself exited 0.
|
|
79
|
+
const PROJECT_CONFIG_RELATIVE_PATH = join('.ai', 'config.json');
|
|
80
|
+
|
|
81
|
+
function globalConfigPath() {
|
|
82
|
+
return join(homedir(), '.claude', 'claude-gates', 'config.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function securityConfigPaths(projectRoot) {
|
|
86
|
+
return [join(projectRoot, PROJECT_CONFIG_RELATIVE_PATH), globalConfigPath()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Reads raw bytes (or null if absent) and their hash, per watched config path. */
|
|
90
|
+
function snapshotConfigs(paths) {
|
|
91
|
+
return paths.map((path) => {
|
|
92
|
+
let content = null;
|
|
93
|
+
try {
|
|
94
|
+
if (existsSync(path)) content = readFileSync(path);
|
|
95
|
+
} catch {
|
|
96
|
+
content = null;
|
|
97
|
+
}
|
|
98
|
+
const hash = content
|
|
99
|
+
? createHash('sha256').update(content).digest('hex')
|
|
100
|
+
: null;
|
|
101
|
+
return { path, content, hash };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Restores every watched config file to its pre-execution content, best-effort. */
|
|
106
|
+
function revertConfigs(snapshots) {
|
|
107
|
+
for (const snapshot of snapshots) {
|
|
108
|
+
try {
|
|
109
|
+
if (snapshot.content === null) {
|
|
110
|
+
if (existsSync(snapshot.path)) unlinkSync(snapshot.path);
|
|
111
|
+
} else {
|
|
112
|
+
writeFileSync(snapshot.path, snapshot.content);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Best-effort revert: if this fails there is nothing more this gate can do
|
|
116
|
+
// beyond having already denied the call.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether any watched config file's content changed since `before`. */
|
|
122
|
+
function configsTampered(before) {
|
|
123
|
+
const after = snapshotConfigs(before.map((snapshot) => snapshot.path));
|
|
124
|
+
return before.some((snapshot, index) => snapshot.hash !== after[index].hash);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
runGate(
|
|
128
|
+
{
|
|
129
|
+
id: GATE_ID,
|
|
130
|
+
configKey: CONFIG_KEY,
|
|
131
|
+
enabledByDefault: false,
|
|
132
|
+
defaultParams: {
|
|
133
|
+
rulesDir: DEFAULT_RULES_DIR,
|
|
134
|
+
gateFileNames: DEFAULT_GATE_FILE_NAMES,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
({ toolName, parameters }) => {
|
|
138
|
+
if (!toolInGroups(toolName, ['execution', 'delegation'])) return;
|
|
139
|
+
|
|
140
|
+
const projectRoot = process.cwd();
|
|
141
|
+
const scripts = discoverScripts(
|
|
142
|
+
projectRoot,
|
|
143
|
+
parameters.rulesDir,
|
|
144
|
+
parameters.gateFileNames,
|
|
145
|
+
);
|
|
146
|
+
if (scripts.length === 0) return;
|
|
147
|
+
|
|
148
|
+
const watchedConfigPaths = securityConfigPaths(projectRoot);
|
|
149
|
+
|
|
150
|
+
for (const script of scripts) {
|
|
151
|
+
const before = snapshotConfigs(watchedConfigPaths);
|
|
152
|
+
|
|
153
|
+
let failed = false;
|
|
154
|
+
try {
|
|
155
|
+
execFileSync(process.execPath, [script], {
|
|
156
|
+
stdio: 'ignore',
|
|
157
|
+
timeout: 5000,
|
|
158
|
+
});
|
|
159
|
+
} catch {
|
|
160
|
+
failed = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (configsTampered(before)) {
|
|
164
|
+
revertConfigs(before);
|
|
165
|
+
deny(
|
|
166
|
+
GATE_ID,
|
|
167
|
+
`Discovered rule/skill script ${relative(projectRoot, script)} attempted to modify gate security configuration (.ai/config.json or the global config). The change was reverted and the action is blocked.`,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (failed) {
|
|
173
|
+
deny(
|
|
174
|
+
GATE_ID,
|
|
175
|
+
`Sub-gate failed: ${relative(projectRoot, script)}. Fix it before continuing.`,
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
);
|