@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,172 +1,179 @@
|
|
|
1
|
-
// Runtime gate configuration for the hooks. Self-contained: Node built-ins only, no npm,
|
|
2
|
-
// so it works when the plugin is installed on its own.
|
|
3
|
-
//
|
|
4
|
-
// This is the READER the gates use at runtime. The CLI (`cli/config.mjs`) is the WRITER;
|
|
5
|
-
// the two never import each other. Both agree on the file: `<project root>/.ai/config.json`,
|
|
6
|
-
// with `~/.claude/claude-gates/config.json` as the global fallback.
|
|
7
|
-
//
|
|
8
|
-
// ── Two things a project controls per gate ──────────────────────────────────────────
|
|
9
|
-
// 1. enabled — is the gate on? A gate absent from config keeps the registry default.
|
|
10
|
-
// 2. params — how the gate behaves: its whitelist, its patterns, its watched paths.
|
|
11
|
-
// A param the project declares REPLACES the gate's built-in default wholesale (it
|
|
12
|
-
// does not merge). The gate ships its defaults in its own source so the user can
|
|
13
|
-
// read them and know exactly what to override.
|
|
14
|
-
//
|
|
15
|
-
// ── Config shape a project may write ────────────────────────────────────────────────
|
|
16
|
-
// "gates": {
|
|
17
|
-
// "blockDestructiveShellCommands": true, // shorthand: enabled only
|
|
18
|
-
// "blockWritesToProtectedPaths": { "enabled": true, "protectedPaths": ["...","..."] }
|
|
19
|
-
// }
|
|
20
|
-
// A bare boolean is the enabled-only shorthand the CLI writes today; an object carries
|
|
21
|
-
// enabled plus any params. Both are accepted so an old plain config keeps working.
|
|
22
|
-
|
|
23
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
|
-
import { dirname, join } from 'node:path';
|
|
26
|
-
|
|
27
|
-
const CLAUDE_USER_DIRECTORY = '.claude';
|
|
28
|
-
const GLOBAL_STATE_DIRECTORY = 'claude-gates';
|
|
29
|
-
const PROJECT_STATE_DIRECTORY = '.ai';
|
|
30
|
-
const CONFIG_FILE = 'config.json';
|
|
31
|
-
// Markers that identify a project root while climbing. Both `.git` AND `.ai/` count — the
|
|
32
|
-
// same set the CLI uses — so a repo-less project (no git yet) still gets its project config
|
|
33
|
-
// read. Anchoring on `.git` alone silently dropped the project layer in such repos.
|
|
34
|
-
const PROJECT_ROOT_MARKERS = ['.git', PROJECT_STATE_DIRECTORY];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
// Runtime gate configuration for the hooks. Self-contained: Node built-ins only, no npm,
|
|
2
|
+
// so it works when the plugin is installed on its own.
|
|
3
|
+
//
|
|
4
|
+
// This is the READER the gates use at runtime. The CLI (`cli/config.mjs`) is the WRITER;
|
|
5
|
+
// the two never import each other. Both agree on the file: `<project root>/.ai/config.json`,
|
|
6
|
+
// with `~/.claude/claude-gates/config.json` as the global fallback.
|
|
7
|
+
//
|
|
8
|
+
// ── Two things a project controls per gate ──────────────────────────────────────────
|
|
9
|
+
// 1. enabled — is the gate on? A gate absent from config keeps the registry default.
|
|
10
|
+
// 2. params — how the gate behaves: its whitelist, its patterns, its watched paths.
|
|
11
|
+
// A param the project declares REPLACES the gate's built-in default wholesale (it
|
|
12
|
+
// does not merge). The gate ships its defaults in its own source so the user can
|
|
13
|
+
// read them and know exactly what to override.
|
|
14
|
+
//
|
|
15
|
+
// ── Config shape a project may write ────────────────────────────────────────────────
|
|
16
|
+
// "gates": {
|
|
17
|
+
// "blockDestructiveShellCommands": true, // shorthand: enabled only
|
|
18
|
+
// "blockWritesToProtectedPaths": { "enabled": true, "protectedPaths": ["...","..."] }
|
|
19
|
+
// }
|
|
20
|
+
// A bare boolean is the enabled-only shorthand the CLI writes today; an object carries
|
|
21
|
+
// enabled plus any params. Both are accepted so an old plain config keeps working.
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
const CLAUDE_USER_DIRECTORY = '.claude';
|
|
28
|
+
const GLOBAL_STATE_DIRECTORY = 'claude-gates';
|
|
29
|
+
const PROJECT_STATE_DIRECTORY = '.ai';
|
|
30
|
+
const CONFIG_FILE = 'config.json';
|
|
31
|
+
// Markers that identify a project root while climbing. Both `.git` AND `.ai/` count — the
|
|
32
|
+
// same set the CLI uses — so a repo-less project (no git yet) still gets its project config
|
|
33
|
+
// read. Anchoring on `.git` alone silently dropped the project layer in such repos.
|
|
34
|
+
const PROJECT_ROOT_MARKERS = ['.git', PROJECT_STATE_DIRECTORY];
|
|
35
|
+
|
|
36
|
+
const BOM_CODE_POINT = 0xfeff;
|
|
37
|
+
|
|
38
|
+
// Strip a leading UTF-8 BOM (U+FEFF) before parsing. `readFileSync(path,'utf8')` does NOT
|
|
39
|
+
// remove it, and JSON.parse throws on a leading BOM — so a config saved by a Windows editor or
|
|
40
|
+
// by PowerShell 5.1's `Set-Content -Encoding utf8` (which prepends a BOM) would be read as
|
|
41
|
+
// unparseable, treated as absent, and SILENTLY DROP every gate disable in it. That is the
|
|
42
|
+
// opposite of safe: it re-enables protections the project turned off, and (worse for the
|
|
43
|
+
// symmetric case) means a project can't be trusted to have been read at all. Removing the
|
|
44
|
+
// BOM makes the common Windows round-trip parse correctly. Compared by char code (not a regex
|
|
45
|
+
// literal) so the BOM never appears as literal irregular whitespace in the source.
|
|
46
|
+
function stripBom(text) {
|
|
47
|
+
return text.charCodeAt(0) === BOM_CODE_POINT ? text.slice(1) : text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJsonOrNull(path) {
|
|
51
|
+
if (!existsSync(path)) return null;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(stripBom(readFileSync(path, 'utf8')));
|
|
54
|
+
} catch {
|
|
55
|
+
// A genuinely corrupt project config must not silently disable protection: treat it as
|
|
56
|
+
// absent, which falls through to the global config and then to the registry defaults.
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Climbs to the nearest project root (a dir holding `.git` or `.ai/`); null when none. */
|
|
62
|
+
function projectRootOf(startDirectory) {
|
|
63
|
+
let current = startDirectory;
|
|
64
|
+
while (true) {
|
|
65
|
+
if (
|
|
66
|
+
PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
|
|
67
|
+
) {
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
const parent = dirname(current);
|
|
71
|
+
if (parent === current) return null;
|
|
72
|
+
current = parent;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function projectConfigPath(startDirectory) {
|
|
77
|
+
const root = projectRootOf(startDirectory);
|
|
78
|
+
if (!root) return null;
|
|
79
|
+
return join(root, PROJECT_STATE_DIRECTORY, CONFIG_FILE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function globalConfigPath(home) {
|
|
83
|
+
return join(home, CLAUDE_USER_DIRECTORY, GLOBAL_STATE_DIRECTORY, CONFIG_FILE);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `gates` object from the first config that exists: project first, then global.
|
|
88
|
+
* A project config that exists but lacks a `gates` key still wins (an empty object) —
|
|
89
|
+
* declaring a config is a deliberate act, and falling through to global would silently
|
|
90
|
+
* re-enable what the project meant to turn off.
|
|
91
|
+
*/
|
|
92
|
+
function gatesLayerFor(startDirectory, home) {
|
|
93
|
+
const projectPath = projectConfigPath(startDirectory);
|
|
94
|
+
const projectData = projectPath ? readJsonOrNull(projectPath) : null;
|
|
95
|
+
if (projectData) return projectData.gates ?? {};
|
|
96
|
+
|
|
97
|
+
const globalData = readJsonOrNull(globalConfigPath(home));
|
|
98
|
+
if (globalData) return globalData.gates ?? {};
|
|
99
|
+
|
|
100
|
+
return null; // nothing declared anywhere: gates fall back to registry defaults
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Normalizes a gate's config entry (bool shorthand or object) to a plain object. */
|
|
104
|
+
function gateEntryOf(gatesLayer, configKey) {
|
|
105
|
+
if (!gatesLayer) return null;
|
|
106
|
+
const entry = gatesLayer[configKey];
|
|
107
|
+
if (entry === undefined) return null;
|
|
108
|
+
if (typeof entry === 'boolean') return { enabled: entry };
|
|
109
|
+
if (entry && typeof entry === 'object') return entry;
|
|
110
|
+
return null; // malformed (string/number/null): ignore, fall back to default
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reads the gates layer once per dispatcher invocation and hands back a lookup the
|
|
115
|
+
* dispatcher and gates share. Reading once matters: N gates in the same tool call must
|
|
116
|
+
* not each re-read and re-parse the file.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} startDirectory usually process.cwd()
|
|
119
|
+
* @returns {{ isEnabled(configKey, registryDefault): boolean,
|
|
120
|
+
* paramsFor(configKey): object }}
|
|
121
|
+
*/
|
|
122
|
+
export function loadGateConfig(startDirectory, { home = homedir() } = {}) {
|
|
123
|
+
const gatesLayer = gatesLayerFor(startDirectory, home);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
/**
|
|
127
|
+
* A gate runs unless a config explicitly turns it off. `enabled` absent means "use
|
|
128
|
+
* the registry default"; only an explicit `false` disables. When no config exists
|
|
129
|
+
* anywhere, the registry default decides.
|
|
130
|
+
*/
|
|
131
|
+
isEnabled(configKey, registryDefault) {
|
|
132
|
+
const entry = gateEntryOf(gatesLayer, configKey);
|
|
133
|
+
if (!entry || entry.enabled === undefined) return registryDefault;
|
|
134
|
+
return entry.enabled !== false;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The project's params for a gate (everything except `enabled`), or an empty object
|
|
139
|
+
* when none are declared. The gate merges these over its own built-in defaults —
|
|
140
|
+
* a declared param replaces the corresponding default wholesale.
|
|
141
|
+
*/
|
|
142
|
+
paramsFor(configKey) {
|
|
143
|
+
const entry = gateEntryOf(gatesLayer, configKey);
|
|
144
|
+
if (!entry) return {};
|
|
145
|
+
const parameters = { ...entry };
|
|
146
|
+
delete parameters.enabled;
|
|
147
|
+
return parameters;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether a gate should run, standalone. `registryDefault` decides when the project (and
|
|
154
|
+
* global) config is silent about this gate; only an explicit `enabled: false` turns a gate
|
|
155
|
+
* off. Used by `runGate` at each gate's start.
|
|
156
|
+
*/
|
|
157
|
+
export function isGateEnabled(
|
|
158
|
+
configKey,
|
|
159
|
+
registryDefault,
|
|
160
|
+
startDirectory,
|
|
161
|
+
{ home = homedir() } = {},
|
|
162
|
+
) {
|
|
163
|
+
return loadGateConfig(startDirectory, { home }).isEnabled(
|
|
164
|
+
configKey,
|
|
165
|
+
registryDefault,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A single gate's project params (its whitelist/patterns), standalone. Returns {} when
|
|
171
|
+
* nothing is declared, so the gate falls back to its own built-in defaults.
|
|
172
|
+
*/
|
|
173
|
+
export function gateParameters(
|
|
174
|
+
configKey,
|
|
175
|
+
startDirectory,
|
|
176
|
+
{ home = homedir() } = {},
|
|
177
|
+
) {
|
|
178
|
+
return loadGateConfig(startDirectory, { home }).paramsFor(configKey);
|
|
179
|
+
}
|