@devrik-tools/claude-gates 0.3.1 → 0.6.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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +25 -4
  3. package/README.md +75 -43
  4. package/cli/config.mjs +126 -124
  5. package/cli/index.mjs +154 -104
  6. package/cli/init.mjs +303 -276
  7. package/cli/install.mjs +281 -175
  8. package/cli/materialize.mjs +103 -102
  9. package/cli/registry.mjs +139 -136
  10. package/cli/smoke-fixtures.json +441 -0
  11. package/cli/smoke.mjs +129 -0
  12. package/cli/task.mjs +140 -140
  13. package/package.json +1 -1
  14. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  15. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  16. package/plugins/gates/hooks/doctor.mjs +207 -207
  17. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  18. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  19. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  20. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  21. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  22. package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
  23. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  24. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  25. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  26. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  27. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  28. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  29. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  30. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  31. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  32. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  33. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  34. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  35. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  36. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  37. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  38. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  39. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  40. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  41. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  42. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  43. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  44. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  45. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  46. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  47. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  48. package/plugins/gates/hooks/hooks.json +51 -0
  49. package/plugins/gates/hooks/lib/config.mjs +179 -172
  50. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  51. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  52. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  53. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  54. package/plugins/tasks/hooks/hooks.json +26 -26
  55. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  56. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  57. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  58. package/registry.json +171 -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
- function readJsonOrNull(path) {
37
- if (!existsSync(path)) return null;
38
- try {
39
- // Strip a UTF-8 BOM () before parsing. `readFileSync(path,'utf8')` does NOT remove
40
- // it, and JSON.parse throws on a leading BOM — so a config saved by a Windows editor or by
41
- // PowerShell 5.1's `Set-Content -Encoding utf8` (which prepends a BOM) would be read as
42
- // unparseable, treated as absent, and SILENTLY DROP every gate disable in it. That is the
43
- // opposite of safe: it re-enables protections the project turned off, and (worse for the
44
- // symmetric case) means a project can't be trusted to have been read at all. Removing the
45
- // BOM makes the common Windows round-trip parse correctly.
46
- return JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
47
- } catch {
48
- // A genuinely corrupt project config must not silently disable protection: treat it as
49
- // absent, which falls through to the global config and then to the registry defaults.
50
- return null;
51
- }
52
- }
53
-
54
- /** Climbs to the nearest project root (a dir holding `.git` or `.ai/`); null when none. */
55
- function projectRootOf(startDirectory) {
56
- let current = startDirectory;
57
- while (true) {
58
- if (
59
- PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
60
- ) {
61
- return current;
62
- }
63
- const parent = dirname(current);
64
- if (parent === current) return null;
65
- current = parent;
66
- }
67
- }
68
-
69
- function projectConfigPath(startDirectory) {
70
- const root = projectRootOf(startDirectory);
71
- if (!root) return null;
72
- return join(root, PROJECT_STATE_DIRECTORY, CONFIG_FILE);
73
- }
74
-
75
- function globalConfigPath(home) {
76
- return join(home, CLAUDE_USER_DIRECTORY, GLOBAL_STATE_DIRECTORY, CONFIG_FILE);
77
- }
78
-
79
- /**
80
- * The `gates` object from the first config that exists: project first, then global.
81
- * A project config that exists but lacks a `gates` key still wins (an empty object) —
82
- * declaring a config is a deliberate act, and falling through to global would silently
83
- * re-enable what the project meant to turn off.
84
- */
85
- function gatesLayerFor(startDirectory, home) {
86
- const projectPath = projectConfigPath(startDirectory);
87
- const projectData = projectPath ? readJsonOrNull(projectPath) : null;
88
- if (projectData) return projectData.gates ?? {};
89
-
90
- const globalData = readJsonOrNull(globalConfigPath(home));
91
- if (globalData) return globalData.gates ?? {};
92
-
93
- return null; // nothing declared anywhere: gates fall back to registry defaults
94
- }
95
-
96
- /** Normalizes a gate's config entry (bool shorthand or object) to a plain object. */
97
- function gateEntryOf(gatesLayer, configKey) {
98
- if (!gatesLayer) return null;
99
- const entry = gatesLayer[configKey];
100
- if (entry === undefined) return null;
101
- if (typeof entry === 'boolean') return { enabled: entry };
102
- if (entry && typeof entry === 'object') return entry;
103
- return null; // malformed (string/number/null): ignore, fall back to default
104
- }
105
-
106
- /**
107
- * Reads the gates layer once per dispatcher invocation and hands back a lookup the
108
- * dispatcher and gates share. Reading once matters: N gates in the same tool call must
109
- * not each re-read and re-parse the file.
110
- *
111
- * @param {string} startDirectory usually process.cwd()
112
- * @returns {{ isEnabled(configKey, registryDefault): boolean,
113
- * paramsFor(configKey): object }}
114
- */
115
- export function loadGateConfig(startDirectory, { home = homedir() } = {}) {
116
- const gatesLayer = gatesLayerFor(startDirectory, home);
117
-
118
- return {
119
- /**
120
- * A gate runs unless a config explicitly turns it off. `enabled` absent means "use
121
- * the registry default"; only an explicit `false` disables. When no config exists
122
- * anywhere, the registry default decides.
123
- */
124
- isEnabled(configKey, registryDefault) {
125
- const entry = gateEntryOf(gatesLayer, configKey);
126
- if (!entry || entry.enabled === undefined) return registryDefault;
127
- return entry.enabled !== false;
128
- },
129
-
130
- /**
131
- * The project's params for a gate (everything except `enabled`), or an empty object
132
- * when none are declared. The gate merges these over its own built-in defaults —
133
- * a declared param replaces the corresponding default wholesale.
134
- */
135
- paramsFor(configKey) {
136
- const entry = gateEntryOf(gatesLayer, configKey);
137
- if (!entry) return {};
138
- const parameters = { ...entry };
139
- delete parameters.enabled;
140
- return parameters;
141
- },
142
- };
143
- }
144
-
145
- /**
146
- * Whether a gate should run, standalone. `registryDefault` decides when the project (and
147
- * global) config is silent about this gate; only an explicit `enabled: false` turns a gate
148
- * off. Used by `runGate` at each gate's start.
149
- */
150
- export function isGateEnabled(
151
- configKey,
152
- registryDefault,
153
- startDirectory,
154
- { home = homedir() } = {},
155
- ) {
156
- return loadGateConfig(startDirectory, { home }).isEnabled(
157
- configKey,
158
- registryDefault,
159
- );
160
- }
161
-
162
- /**
163
- * A single gate's project params (its whitelist/patterns), standalone. Returns {} when
164
- * nothing is declared, so the gate falls back to its own built-in defaults.
165
- */
166
- export function gateParameters(
167
- configKey,
168
- startDirectory,
169
- { home = homedir() } = {},
170
- ) {
171
- return loadGateConfig(startDirectory, { home }).paramsFor(configKey);
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
+ }