@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,149 +1,152 @@
|
|
|
1
|
-
// lint-commit — denies `git commit` while the project's lint script fails. The gate never
|
|
2
|
-
// invents a lint step: it only runs a `lint` script the project itself declared in
|
|
3
|
-
// package.json (or an explicit override), so a project with no linter configured is left
|
|
4
|
-
// alone (allow, silent).
|
|
5
|
-
//
|
|
6
|
-
// justification: no existing tool covers this. bash-commands (the other shell gate) only
|
|
7
|
-
// pattern-matches the command text; it never SPAWNS a check. This is the first gate that
|
|
8
|
-
// runs an external process as its verdict.
|
|
9
|
-
//
|
|
10
|
-
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
11
|
-
// lintCommand explicit command to run instead of autodetecting `npm run lint`.
|
|
12
|
-
// null (default) means: read package.json's `scripts.lint`, and if
|
|
13
|
-
// absent, allow — this gate does not invent a lint step.
|
|
14
|
-
// lintTimeoutMs how long the lint run may take before it is treated as a failure.
|
|
15
|
-
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
16
|
-
// its override replaces.
|
|
17
|
-
//
|
|
18
|
-
// ── How it detects a commit ──────────────────────────────────────────────────────────
|
|
19
|
-
// Reuses bash-commands' git-global-option normalization idea: `git -C <path> commit` or
|
|
20
|
-
// `git -c user.name=x commit` must be caught the same as a bare `git commit`. Only a real
|
|
21
|
-
// commit (not `git commit --help`, not a mention inside a delegation prompt describing one)
|
|
22
|
-
// is gated — this runs on the shell tool only, not on delegation text, because spawning a
|
|
23
|
-
// lint run for a prompt that merely MENTIONS a commit would be wasted work and a false
|
|
24
|
-
// blocker on unrelated delegations.
|
|
25
|
-
//
|
|
26
|
-
// ── Why spawnSync + shell:true on Windows ────────────────────────────────────────────
|
|
27
|
-
// `npm` is `npm.cmd` on Windows; resolving the concrete binary name per platform is more
|
|
28
|
-
// fragile than letting the shell resolve it, so this always spawns through the shell.
|
|
29
|
-
//
|
|
30
|
-
// ── Fail-safe shape ───────────────────────────────────────────────────────────────────
|
|
31
|
-
// No package.json, or package.json with no `scripts.lint` and no `lintCommand` override:
|
|
32
|
-
// allow (nothing to enforce, never invented). Lint run exits non-zero: deny with the last
|
|
33
|
-
// ~15 lines of its combined output. Lint run exits zero: allow.
|
|
34
|
-
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { join } from 'node:path';
|
|
38
|
-
import { runGate, deny, toolInGroups } from '../../lib/hook-io.mjs';
|
|
39
|
-
|
|
40
|
-
const GATE_ID = 'lint-commit';
|
|
41
|
-
const CONFIG_KEY = 'blockCommitWithFailingLint';
|
|
42
|
-
|
|
43
|
-
const SHELL_GROUPS = ['shell'];
|
|
44
|
-
const DEFAULT_LINT_TIMEOUT_MS = 60000;
|
|
45
|
-
const OUTPUT_TAIL_LINES = 15;
|
|
46
|
-
|
|
47
|
-
// One global git option at a time, stripped repeatedly — same normalization bash-commands
|
|
48
|
-
// uses so `git -C /repo -c x=y commit` reduces to `git commit` before the pattern runs.
|
|
49
|
-
const GIT_OPTION_WITH_VALUE = String.raw`(?:-[Cc]|--git-dir|--work-tree|--namespace|--exec-path|--config-env)(?:\s+|=)\S+`;
|
|
50
|
-
const GIT_FLAG_OPTION = String.raw`--(?:paginate|no-pager|bare|no-optional-locks)|-p`;
|
|
51
|
-
const GIT_GLOBAL_OPTION_PATTERN = new RegExp(
|
|
52
|
-
String.raw`\bgit\s+(?:${GIT_OPTION_WITH_VALUE}|${GIT_FLAG_OPTION})\s+`,
|
|
53
|
-
'i',
|
|
54
|
-
);
|
|
55
|
-
const GIT_COMMIT_PATTERN = /\bgit\s+commit\b/i;
|
|
56
|
-
|
|
57
|
-
function normalizeGitOptions(command) {
|
|
58
|
-
let previous;
|
|
59
|
-
let normalized = command;
|
|
60
|
-
do {
|
|
61
|
-
previous = normalized;
|
|
62
|
-
normalized = normalized.replace(GIT_GLOBAL_OPTION_PATTERN, 'git ');
|
|
63
|
-
} while (normalized !== previous);
|
|
64
|
-
return normalized;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isGitCommit(command) {
|
|
68
|
-
return GIT_COMMIT_PATTERN.test(normalizeGitOptions(command));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function commandTextFrom(toolInput) {
|
|
72
|
-
return String(toolInput.CommandLine ?? toolInput.command ?? '');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** The project's declared lint command, or null when none is declared (never invented). */
|
|
76
|
-
function resolveLintCommand(cwd, lintCommandOverride) {
|
|
77
|
-
if (lintCommandOverride) return lintCommandOverride;
|
|
78
|
-
|
|
79
|
-
const packageJsonPath = join(cwd, 'package.json');
|
|
80
|
-
if (!existsSync(packageJsonPath)) return null;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
84
|
-
if (packageJson?.scripts?.lint) return 'npm run lint';
|
|
85
|
-
return null;
|
|
86
|
-
} catch {
|
|
87
|
-
// Corrupt package.json is not this gate's problem to diagnose; do not invent a lint step.
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function tailLines(text, count) {
|
|
93
|
-
const lines = String(text)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
deny
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
);
|
|
1
|
+
// lint-commit — denies `git commit` while the project's lint script fails. The gate never
|
|
2
|
+
// invents a lint step: it only runs a `lint` script the project itself declared in
|
|
3
|
+
// package.json (or an explicit override), so a project with no linter configured is left
|
|
4
|
+
// alone (allow, silent).
|
|
5
|
+
//
|
|
6
|
+
// justification: no existing tool covers this. bash-commands (the other shell gate) only
|
|
7
|
+
// pattern-matches the command text; it never SPAWNS a check. This is the first gate that
|
|
8
|
+
// runs an external process as its verdict.
|
|
9
|
+
//
|
|
10
|
+
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
11
|
+
// lintCommand explicit command to run instead of autodetecting `npm run lint`.
|
|
12
|
+
// null (default) means: read package.json's `scripts.lint`, and if
|
|
13
|
+
// absent, allow — this gate does not invent a lint step.
|
|
14
|
+
// lintTimeoutMs how long the lint run may take before it is treated as a failure.
|
|
15
|
+
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
16
|
+
// its override replaces.
|
|
17
|
+
//
|
|
18
|
+
// ── How it detects a commit ──────────────────────────────────────────────────────────
|
|
19
|
+
// Reuses bash-commands' git-global-option normalization idea: `git -C <path> commit` or
|
|
20
|
+
// `git -c user.name=x commit` must be caught the same as a bare `git commit`. Only a real
|
|
21
|
+
// commit (not `git commit --help`, not a mention inside a delegation prompt describing one)
|
|
22
|
+
// is gated — this runs on the shell tool only, not on delegation text, because spawning a
|
|
23
|
+
// lint run for a prompt that merely MENTIONS a commit would be wasted work and a false
|
|
24
|
+
// blocker on unrelated delegations.
|
|
25
|
+
//
|
|
26
|
+
// ── Why spawnSync + shell:true on Windows ────────────────────────────────────────────
|
|
27
|
+
// `npm` is `npm.cmd` on Windows; resolving the concrete binary name per platform is more
|
|
28
|
+
// fragile than letting the shell resolve it, so this always spawns through the shell.
|
|
29
|
+
//
|
|
30
|
+
// ── Fail-safe shape ───────────────────────────────────────────────────────────────────
|
|
31
|
+
// No package.json, or package.json with no `scripts.lint` and no `lintCommand` override:
|
|
32
|
+
// allow (nothing to enforce, never invented). Lint run exits non-zero: deny with the last
|
|
33
|
+
// ~15 lines of its combined output. Lint run exits zero: allow.
|
|
34
|
+
|
|
35
|
+
import { spawnSync } from 'node:child_process';
|
|
36
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
37
|
+
import { join } from 'node:path';
|
|
38
|
+
import { runGate, deny, toolInGroups } from '../../lib/hook-io.mjs';
|
|
39
|
+
|
|
40
|
+
const GATE_ID = 'lint-commit';
|
|
41
|
+
const CONFIG_KEY = 'blockCommitWithFailingLint';
|
|
42
|
+
|
|
43
|
+
const SHELL_GROUPS = ['shell'];
|
|
44
|
+
const DEFAULT_LINT_TIMEOUT_MS = 60000;
|
|
45
|
+
const OUTPUT_TAIL_LINES = 15;
|
|
46
|
+
|
|
47
|
+
// One global git option at a time, stripped repeatedly — same normalization bash-commands
|
|
48
|
+
// uses so `git -C /repo -c x=y commit` reduces to `git commit` before the pattern runs.
|
|
49
|
+
const GIT_OPTION_WITH_VALUE = String.raw`(?:-[Cc]|--git-dir|--work-tree|--namespace|--exec-path|--config-env)(?:\s+|=)\S+`;
|
|
50
|
+
const GIT_FLAG_OPTION = String.raw`--(?:paginate|no-pager|bare|no-optional-locks)|-p`;
|
|
51
|
+
const GIT_GLOBAL_OPTION_PATTERN = new RegExp(
|
|
52
|
+
String.raw`\bgit\s+(?:${GIT_OPTION_WITH_VALUE}|${GIT_FLAG_OPTION})\s+`,
|
|
53
|
+
'i',
|
|
54
|
+
);
|
|
55
|
+
const GIT_COMMIT_PATTERN = /\bgit\s+commit\b/i;
|
|
56
|
+
|
|
57
|
+
function normalizeGitOptions(command) {
|
|
58
|
+
let previous;
|
|
59
|
+
let normalized = command;
|
|
60
|
+
do {
|
|
61
|
+
previous = normalized;
|
|
62
|
+
normalized = normalized.replace(GIT_GLOBAL_OPTION_PATTERN, 'git ');
|
|
63
|
+
} while (normalized !== previous);
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isGitCommit(command) {
|
|
68
|
+
return GIT_COMMIT_PATTERN.test(normalizeGitOptions(command));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function commandTextFrom(toolInput) {
|
|
72
|
+
return String(toolInput.CommandLine ?? toolInput.command ?? '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The project's declared lint command, or null when none is declared (never invented). */
|
|
76
|
+
function resolveLintCommand(cwd, lintCommandOverride) {
|
|
77
|
+
if (lintCommandOverride) return lintCommandOverride;
|
|
78
|
+
|
|
79
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
80
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
84
|
+
if (packageJson?.scripts?.lint) return 'npm run lint';
|
|
85
|
+
return null;
|
|
86
|
+
} catch {
|
|
87
|
+
// Corrupt package.json is not this gate's problem to diagnose; do not invent a lint step.
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tailLines(text, count) {
|
|
93
|
+
const lines = String(text)
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.filter((line) => line.length > 0);
|
|
96
|
+
return lines.slice(-count).join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function runLint(lintCommand, cwd, timeoutMs) {
|
|
100
|
+
return spawnSync(lintCommand, {
|
|
101
|
+
cwd,
|
|
102
|
+
shell: true,
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
timeout: timeoutMs,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
runGate(
|
|
109
|
+
{
|
|
110
|
+
id: GATE_ID,
|
|
111
|
+
configKey: CONFIG_KEY,
|
|
112
|
+
enabledByDefault: false,
|
|
113
|
+
defaultParams: {
|
|
114
|
+
lintCommand: null,
|
|
115
|
+
lintTimeoutMs: DEFAULT_LINT_TIMEOUT_MS,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
({ toolName, toolInput, parameters }) => {
|
|
119
|
+
if (!toolInGroups(toolName, SHELL_GROUPS)) return;
|
|
120
|
+
|
|
121
|
+
const command = commandTextFrom(toolInput);
|
|
122
|
+
if (!isGitCommit(command)) return;
|
|
123
|
+
|
|
124
|
+
const cwd = process.cwd();
|
|
125
|
+
const lintCommand = resolveLintCommand(cwd, parameters.lintCommand);
|
|
126
|
+
if (!lintCommand) return; // no lint script declared anywhere: nothing to enforce
|
|
127
|
+
|
|
128
|
+
const timeoutMs = parameters.lintTimeoutMs ?? DEFAULT_LINT_TIMEOUT_MS;
|
|
129
|
+
const result = runLint(lintCommand, cwd, timeoutMs);
|
|
130
|
+
|
|
131
|
+
if (result.error) {
|
|
132
|
+
// The lint process itself could not be spawned (bad shell, missing interpreter, or a
|
|
133
|
+
// timeout kill). A commit gate that cannot evaluate must not silently permit — deny
|
|
134
|
+
// with the spawn error so the user can fix the environment, not guess.
|
|
135
|
+
deny(
|
|
136
|
+
GATE_ID,
|
|
137
|
+
`Could not run the lint command ("${lintCommand}"): ${result.error.message}. Fix ` +
|
|
138
|
+
'the lint setup or set lintCommand/blockCommitWithFailingLint in .ai/config.json.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (result.status !== 0) {
|
|
143
|
+
const combinedOutput =
|
|
144
|
+
`${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
145
|
+
deny(
|
|
146
|
+
GATE_ID,
|
|
147
|
+
`Lint failed (exit ${result.status}) — commit blocked until it passes:\n` +
|
|
148
|
+
`${tailLines(combinedOutput, OUTPUT_TAIL_LINES)}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
);
|