@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.es.md +25 -4
- package/README.md +75 -43
- package/cli/config.mjs +126 -124
- package/cli/index.mjs +154 -104
- 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 +441 -0
- package/cli/smoke.mjs +129 -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-before-delegate/index.mjs +269 -265
- package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -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 +51 -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 +171 -1
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
// wiring-check — SessionStart hook. Cross-checks registry.json, each plugin's hooks.json and
|
|
2
|
-
// the scripts on disk, and warns (never blocks — SessionStart cannot deny) when they
|
|
3
|
-
// disagree. This is the check that would have caught the session family itself being
|
|
4
|
-
// declared in registry.json with no script and no hooks.json wiring: three checks,
|
|
5
|
-
// each catching a different half of a broken install —
|
|
6
|
-
//
|
|
7
|
-
// 1. registry gate -> not wired: a gate.json entry exists but no hooks.json (of the
|
|
8
|
-
// family's plugin) references its script for the declared event.
|
|
9
|
-
// 2. hooks.json command -> script missing: a hooks.json entry points at a script path
|
|
10
|
-
// that does not exist on disk.
|
|
11
|
-
// 3. orphaned gate folder: a `gates/<id>/index.mjs` exists under a plugin's hooks/ dir
|
|
12
|
-
// but registry.json has no gate pointing at it.
|
|
13
|
-
//
|
|
14
|
-
// Consistent (nothing to report) -> silent. Fail-safe: wrapped in try/catch, a bug here
|
|
15
|
-
// must never block session start.
|
|
16
|
-
//
|
|
17
|
-
// justification: no existing tool covers this. `cli/__tests__/registry-gates-consistency
|
|
18
|
-
// .test.mjs` checks registry-vs-disk at CI/dev time; this is the SAME class of check
|
|
19
|
-
// surfaced live, at session start, in an already-installed plugin where the source
|
|
20
|
-
// checkout (and that test) is not present.
|
|
21
|
-
|
|
22
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
|
-
import { dirname, join } from 'node:path';
|
|
25
|
-
import { fileURLToPath } from 'node:url';
|
|
26
|
-
|
|
27
|
-
const STDIN_FILE_DESCRIPTOR = 0;
|
|
28
|
-
const SESSION_START_EVENT = 'SessionStart';
|
|
29
|
-
const CONFIG_KEY = 'checkWiringOnStart';
|
|
30
|
-
|
|
31
|
-
const HOOKS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const REPOSITORY_ROOT = join(HOOKS_DIRECTORY, '..', '..', '..');
|
|
33
|
-
const REGISTRY_PATH = join(REPOSITORY_ROOT, 'registry.json');
|
|
34
|
-
const DEFAULT_PLUGIN = 'gates';
|
|
35
|
-
|
|
36
|
-
const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
|
|
37
|
-
const PROJECT_CONFIG = join('.ai', 'config.json');
|
|
38
|
-
const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
|
|
39
|
-
|
|
40
|
-
const BOM_CHAR_CODE = 0xfeff;
|
|
41
|
-
|
|
42
|
-
/** Strips a UTF-8 BOM a Windows editor/tool may have written before the JSON. */
|
|
43
|
-
function stripBom(text) {
|
|
44
|
-
return text.charCodeAt(0) === BOM_CHAR_CODE ? text.slice(1) : text;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function readJson(path) {
|
|
48
|
-
try {
|
|
49
|
-
return JSON.parse(stripBom(readFileSync(path, 'utf8')));
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
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
|
-
/** The wiring-check gate config entry (project overrides global), or null. */
|
|
70
|
-
function wiringConfig(startDirectory) {
|
|
71
|
-
const root = projectRootOf(startDirectory);
|
|
72
|
-
const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
|
|
73
|
-
const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
|
|
74
|
-
const layer = projectData?.gates ?? globalData?.gates ?? {};
|
|
75
|
-
const entry = layer[CONFIG_KEY];
|
|
76
|
-
if (typeof entry === 'boolean') return { enabled: entry };
|
|
77
|
-
if (entry && typeof entry === 'object') return entry;
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function readPayload() {
|
|
82
|
-
try {
|
|
83
|
-
return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
|
|
84
|
-
} catch {
|
|
85
|
-
return {};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function pluginHooksDirectory(pluginName) {
|
|
90
|
-
return join(REPOSITORY_ROOT, 'plugins', pluginName, 'hooks');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function pluginHooksJsonPath(pluginName) {
|
|
94
|
-
return join(pluginHooksDirectory(pluginName), 'hooks.json');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Every hooks.json command string across all its events, flattened. */
|
|
98
|
-
function commandsIn(hooksJson) {
|
|
99
|
-
if (!hooksJson?.hooks) return [];
|
|
100
|
-
const commands = [];
|
|
101
|
-
for (const entries of Object.values(hooksJson.hooks)) {
|
|
102
|
-
for (const entry of entries ?? []) {
|
|
103
|
-
for (const hook of entry?.hooks ?? []) {
|
|
104
|
-
if (hook?.command) commands.push(hook.command);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return commands;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Whether some hooks.json command references this script's basename for this plugin. */
|
|
112
|
-
function scriptIsWired(commands, script) {
|
|
113
|
-
// Commands look like `node "${CLAUDE_PLUGIN_ROOT}/hooks/gates/x/index.mjs"` — match on
|
|
114
|
-
// the script's path-with-forward-slashes so it is independent of the wrapping command
|
|
115
|
-
// shape (quoting, the node invocation, timeout, etc.).
|
|
116
|
-
const needle = script.replace(/\\/g, '/');
|
|
117
|
-
return commands.some((command) =>
|
|
118
|
-
command.replace(/\\/g, '/').includes(needle),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function checkRegistryVsHooksJson(registry) {
|
|
123
|
-
const problems = [];
|
|
124
|
-
const hooksJsonCache = new Map();
|
|
125
|
-
|
|
126
|
-
for (const family of registry.families ?? []) {
|
|
127
|
-
const pluginName = family.plugin ?? DEFAULT_PLUGIN;
|
|
128
|
-
if (!hooksJsonCache.has(pluginName)) {
|
|
129
|
-
hooksJsonCache.set(pluginName, readJson(pluginHooksJsonPath(pluginName)));
|
|
130
|
-
}
|
|
131
|
-
const hooksJson = hooksJsonCache.get(pluginName);
|
|
132
|
-
const commands = commandsIn(hooksJson);
|
|
133
|
-
|
|
134
|
-
for (const gate of family.gates ?? []) {
|
|
135
|
-
const scriptPath = join(pluginHooksDirectory(pluginName), gate.script);
|
|
136
|
-
if (!existsSync(scriptPath)) {
|
|
137
|
-
problems.push(
|
|
138
|
-
`gate "${gate.id}": script missing on disk (expected plugins/${pluginName}/hooks/${gate.script})`,
|
|
139
|
-
);
|
|
140
|
-
continue; // no point checking wiring for a script that isn't even there
|
|
141
|
-
}
|
|
142
|
-
if (!scriptIsWired(commands, gate.script)) {
|
|
143
|
-
problems.push(
|
|
144
|
-
`gate "${gate.id}": not wired in plugins/${pluginName}/hooks/hooks.json (${gate.event})`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return problems;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Gate folders (gates/<id>/index.mjs) on disk with no registry entry pointing at them. */
|
|
153
|
-
function orphanedGateFolders(registry) {
|
|
154
|
-
const declaredScripts = new Set(
|
|
155
|
-
(registry.families ?? []).flatMap((family) =>
|
|
156
|
-
(family.gates ?? [])
|
|
157
|
-
.filter((gate) => gate.script.startsWith('gates/'))
|
|
158
|
-
.map((gate) => `${family.plugin ?? DEFAULT_PLUGIN}::${gate.script}`),
|
|
159
|
-
),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const problems = [];
|
|
163
|
-
const pluginNames = new Set(
|
|
164
|
-
(registry.families ?? []).map((family) => family.plugin ?? DEFAULT_PLUGIN),
|
|
165
|
-
);
|
|
166
|
-
for (const pluginName of pluginNames) {
|
|
167
|
-
const gatesDirectory = join(pluginHooksDirectory(pluginName), 'gates');
|
|
168
|
-
if (!existsSync(gatesDirectory)) continue;
|
|
169
|
-
let entries;
|
|
170
|
-
try {
|
|
171
|
-
entries = readdirSync(gatesDirectory, { withFileTypes: true });
|
|
172
|
-
} catch {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
for (const entry of entries) {
|
|
176
|
-
const script = `gates/${entry.name}/index.mjs`;
|
|
177
|
-
const isRealGateFolder =
|
|
178
|
-
entry.isDirectory() &&
|
|
179
|
-
existsSync(join(gatesDirectory, entry.name, 'index.mjs'));
|
|
180
|
-
if (!isRealGateFolder) continue;
|
|
181
|
-
if (!declaredScripts.has(`${pluginName}::${script}`)) {
|
|
182
|
-
problems.push(
|
|
183
|
-
`plugins/${pluginName}/hooks/gates/${entry.name}/index.mjs has no registry entry`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return problems;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function speak(context) {
|
|
192
|
-
process.stdout.write(
|
|
193
|
-
JSON.stringify({
|
|
194
|
-
hookSpecificOutput: {
|
|
195
|
-
hookEventName: SESSION_START_EVENT,
|
|
196
|
-
additionalContext: context,
|
|
197
|
-
},
|
|
198
|
-
}),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function main() {
|
|
203
|
-
const payload = readPayload();
|
|
204
|
-
const cwd = payload.cwd || process.cwd();
|
|
205
|
-
|
|
206
|
-
const config = wiringConfig(cwd);
|
|
207
|
-
if (config && config.enabled === false) return; // gate turned off for this project
|
|
208
|
-
|
|
209
|
-
const registry = readJson(REGISTRY_PATH);
|
|
210
|
-
if (!registry?.families) return; // can't read the registry: nothing to report
|
|
211
|
-
|
|
212
|
-
const problems = [
|
|
213
|
-
...checkRegistryVsHooksJson(registry),
|
|
214
|
-
...orphanedGateFolders(registry),
|
|
215
|
-
];
|
|
216
|
-
if (problems.length === 0) return; // everything consistent: stay silent
|
|
217
|
-
|
|
218
|
-
speak(
|
|
219
|
-
`[wiring-check] claude-gates wiring inconsistencies found:\n- ${problems.join('\n- ')}`,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
main();
|
|
225
|
-
} catch {
|
|
226
|
-
// A wiring-check bug must never block session start.
|
|
227
|
-
}
|
|
1
|
+
// wiring-check — SessionStart hook. Cross-checks registry.json, each plugin's hooks.json and
|
|
2
|
+
// the scripts on disk, and warns (never blocks — SessionStart cannot deny) when they
|
|
3
|
+
// disagree. This is the check that would have caught the session family itself being
|
|
4
|
+
// declared in registry.json with no script and no hooks.json wiring: three checks,
|
|
5
|
+
// each catching a different half of a broken install —
|
|
6
|
+
//
|
|
7
|
+
// 1. registry gate -> not wired: a gate.json entry exists but no hooks.json (of the
|
|
8
|
+
// family's plugin) references its script for the declared event.
|
|
9
|
+
// 2. hooks.json command -> script missing: a hooks.json entry points at a script path
|
|
10
|
+
// that does not exist on disk.
|
|
11
|
+
// 3. orphaned gate folder: a `gates/<id>/index.mjs` exists under a plugin's hooks/ dir
|
|
12
|
+
// but registry.json has no gate pointing at it.
|
|
13
|
+
//
|
|
14
|
+
// Consistent (nothing to report) -> silent. Fail-safe: wrapped in try/catch, a bug here
|
|
15
|
+
// must never block session start.
|
|
16
|
+
//
|
|
17
|
+
// justification: no existing tool covers this. `cli/__tests__/registry-gates-consistency
|
|
18
|
+
// .test.mjs` checks registry-vs-disk at CI/dev time; this is the SAME class of check
|
|
19
|
+
// surfaced live, at session start, in an already-installed plugin where the source
|
|
20
|
+
// checkout (and that test) is not present.
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const STDIN_FILE_DESCRIPTOR = 0;
|
|
28
|
+
const SESSION_START_EVENT = 'SessionStart';
|
|
29
|
+
const CONFIG_KEY = 'checkWiringOnStart';
|
|
30
|
+
|
|
31
|
+
const HOOKS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const REPOSITORY_ROOT = join(HOOKS_DIRECTORY, '..', '..', '..');
|
|
33
|
+
const REGISTRY_PATH = join(REPOSITORY_ROOT, 'registry.json');
|
|
34
|
+
const DEFAULT_PLUGIN = 'gates';
|
|
35
|
+
|
|
36
|
+
const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
|
|
37
|
+
const PROJECT_CONFIG = join('.ai', 'config.json');
|
|
38
|
+
const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
|
|
39
|
+
|
|
40
|
+
const BOM_CHAR_CODE = 0xfeff;
|
|
41
|
+
|
|
42
|
+
/** Strips a UTF-8 BOM a Windows editor/tool may have written before the JSON. */
|
|
43
|
+
function stripBom(text) {
|
|
44
|
+
return text.charCodeAt(0) === BOM_CHAR_CODE ? text.slice(1) : text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJson(path) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(stripBom(readFileSync(path, 'utf8')));
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
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
|
+
/** The wiring-check gate config entry (project overrides global), or null. */
|
|
70
|
+
function wiringConfig(startDirectory) {
|
|
71
|
+
const root = projectRootOf(startDirectory);
|
|
72
|
+
const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
|
|
73
|
+
const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
|
|
74
|
+
const layer = projectData?.gates ?? globalData?.gates ?? {};
|
|
75
|
+
const entry = layer[CONFIG_KEY];
|
|
76
|
+
if (typeof entry === 'boolean') return { enabled: entry };
|
|
77
|
+
if (entry && typeof entry === 'object') return entry;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readPayload() {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
|
|
84
|
+
} catch {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pluginHooksDirectory(pluginName) {
|
|
90
|
+
return join(REPOSITORY_ROOT, 'plugins', pluginName, 'hooks');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pluginHooksJsonPath(pluginName) {
|
|
94
|
+
return join(pluginHooksDirectory(pluginName), 'hooks.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every hooks.json command string across all its events, flattened. */
|
|
98
|
+
function commandsIn(hooksJson) {
|
|
99
|
+
if (!hooksJson?.hooks) return [];
|
|
100
|
+
const commands = [];
|
|
101
|
+
for (const entries of Object.values(hooksJson.hooks)) {
|
|
102
|
+
for (const entry of entries ?? []) {
|
|
103
|
+
for (const hook of entry?.hooks ?? []) {
|
|
104
|
+
if (hook?.command) commands.push(hook.command);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return commands;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether some hooks.json command references this script's basename for this plugin. */
|
|
112
|
+
function scriptIsWired(commands, script) {
|
|
113
|
+
// Commands look like `node "${CLAUDE_PLUGIN_ROOT}/hooks/gates/x/index.mjs"` — match on
|
|
114
|
+
// the script's path-with-forward-slashes so it is independent of the wrapping command
|
|
115
|
+
// shape (quoting, the node invocation, timeout, etc.).
|
|
116
|
+
const needle = script.replace(/\\/g, '/');
|
|
117
|
+
return commands.some((command) =>
|
|
118
|
+
command.replace(/\\/g, '/').includes(needle),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function checkRegistryVsHooksJson(registry) {
|
|
123
|
+
const problems = [];
|
|
124
|
+
const hooksJsonCache = new Map();
|
|
125
|
+
|
|
126
|
+
for (const family of registry.families ?? []) {
|
|
127
|
+
const pluginName = family.plugin ?? DEFAULT_PLUGIN;
|
|
128
|
+
if (!hooksJsonCache.has(pluginName)) {
|
|
129
|
+
hooksJsonCache.set(pluginName, readJson(pluginHooksJsonPath(pluginName)));
|
|
130
|
+
}
|
|
131
|
+
const hooksJson = hooksJsonCache.get(pluginName);
|
|
132
|
+
const commands = commandsIn(hooksJson);
|
|
133
|
+
|
|
134
|
+
for (const gate of family.gates ?? []) {
|
|
135
|
+
const scriptPath = join(pluginHooksDirectory(pluginName), gate.script);
|
|
136
|
+
if (!existsSync(scriptPath)) {
|
|
137
|
+
problems.push(
|
|
138
|
+
`gate "${gate.id}": script missing on disk (expected plugins/${pluginName}/hooks/${gate.script})`,
|
|
139
|
+
);
|
|
140
|
+
continue; // no point checking wiring for a script that isn't even there
|
|
141
|
+
}
|
|
142
|
+
if (!scriptIsWired(commands, gate.script)) {
|
|
143
|
+
problems.push(
|
|
144
|
+
`gate "${gate.id}": not wired in plugins/${pluginName}/hooks/hooks.json (${gate.event})`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return problems;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Gate folders (gates/<id>/index.mjs) on disk with no registry entry pointing at them. */
|
|
153
|
+
function orphanedGateFolders(registry) {
|
|
154
|
+
const declaredScripts = new Set(
|
|
155
|
+
(registry.families ?? []).flatMap((family) =>
|
|
156
|
+
(family.gates ?? [])
|
|
157
|
+
.filter((gate) => gate.script.startsWith('gates/'))
|
|
158
|
+
.map((gate) => `${family.plugin ?? DEFAULT_PLUGIN}::${gate.script}`),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const problems = [];
|
|
163
|
+
const pluginNames = new Set(
|
|
164
|
+
(registry.families ?? []).map((family) => family.plugin ?? DEFAULT_PLUGIN),
|
|
165
|
+
);
|
|
166
|
+
for (const pluginName of pluginNames) {
|
|
167
|
+
const gatesDirectory = join(pluginHooksDirectory(pluginName), 'gates');
|
|
168
|
+
if (!existsSync(gatesDirectory)) continue;
|
|
169
|
+
let entries;
|
|
170
|
+
try {
|
|
171
|
+
entries = readdirSync(gatesDirectory, { withFileTypes: true });
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const script = `gates/${entry.name}/index.mjs`;
|
|
177
|
+
const isRealGateFolder =
|
|
178
|
+
entry.isDirectory() &&
|
|
179
|
+
existsSync(join(gatesDirectory, entry.name, 'index.mjs'));
|
|
180
|
+
if (!isRealGateFolder) continue;
|
|
181
|
+
if (!declaredScripts.has(`${pluginName}::${script}`)) {
|
|
182
|
+
problems.push(
|
|
183
|
+
`plugins/${pluginName}/hooks/gates/${entry.name}/index.mjs has no registry entry`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return problems;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function speak(context) {
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
hookSpecificOutput: {
|
|
195
|
+
hookEventName: SESSION_START_EVENT,
|
|
196
|
+
additionalContext: context,
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function main() {
|
|
203
|
+
const payload = readPayload();
|
|
204
|
+
const cwd = payload.cwd || process.cwd();
|
|
205
|
+
|
|
206
|
+
const config = wiringConfig(cwd);
|
|
207
|
+
if (config && config.enabled === false) return; // gate turned off for this project
|
|
208
|
+
|
|
209
|
+
const registry = readJson(REGISTRY_PATH);
|
|
210
|
+
if (!registry?.families) return; // can't read the registry: nothing to report
|
|
211
|
+
|
|
212
|
+
const problems = [
|
|
213
|
+
...checkRegistryVsHooksJson(registry),
|
|
214
|
+
...orphanedGateFolders(registry),
|
|
215
|
+
];
|
|
216
|
+
if (problems.length === 0) return; // everything consistent: stay silent
|
|
217
|
+
|
|
218
|
+
speak(
|
|
219
|
+
`[wiring-check] claude-gates wiring inconsistencies found:\n- ${problems.join('\n- ')}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
main();
|
|
225
|
+
} catch {
|
|
226
|
+
// A wiring-check bug must never block session start.
|
|
227
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tasks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Deterministic task tracking for Claude Code: persists tasks the model registers via the CLI, reminds of open tasks on a message counter, and lists active tasks on session start.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"UserPromptSubmit": [
|
|
4
|
-
{
|
|
5
|
-
"hooks": [
|
|
6
|
-
{
|
|
7
|
-
"type": "command",
|
|
8
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/register-requests.mjs\"",
|
|
9
|
-
"timeout": 10
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"SessionStart": [
|
|
15
|
-
{
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-tasks.mjs\"",
|
|
20
|
-
"timeout": 10
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/register-requests.mjs\"",
|
|
9
|
+
"timeout": 10
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"SessionStart": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-tasks.mjs\"",
|
|
20
|
+
"timeout": 10
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|