@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,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 { existsSync, readFileSync } from 'node:fs';
36
- import { spawnSync } from 'node:child_process';
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).split(/\r?\n/).filter((line) => line.length > 0);
94
- return lines.slice(-count).join('\n');
95
- }
96
-
97
- function runLint(lintCommand, cwd, timeoutMs) {
98
- return spawnSync(lintCommand, {
99
- cwd,
100
- shell: true,
101
- encoding: 'utf8',
102
- timeout: timeoutMs,
103
- });
104
- }
105
-
106
- runGate(
107
- {
108
- id: GATE_ID,
109
- configKey: CONFIG_KEY,
110
- enabledByDefault: false,
111
- defaultParams: {
112
- lintCommand: null,
113
- lintTimeoutMs: DEFAULT_LINT_TIMEOUT_MS,
114
- },
115
- },
116
- ({ toolName, toolInput, parameters }) => {
117
- if (!toolInGroups(toolName, SHELL_GROUPS)) return;
118
-
119
- const command = commandTextFrom(toolInput);
120
- if (!isGitCommit(command)) return;
121
-
122
- const cwd = process.cwd();
123
- const lintCommand = resolveLintCommand(cwd, parameters.lintCommand);
124
- if (!lintCommand) return; // no lint script declared anywhere: nothing to enforce
125
-
126
- const timeoutMs = parameters.lintTimeoutMs ?? DEFAULT_LINT_TIMEOUT_MS;
127
- const result = runLint(lintCommand, cwd, timeoutMs);
128
-
129
- if (result.error) {
130
- // The lint process itself could not be spawned (bad shell, missing interpreter, or a
131
- // timeout kill). A commit gate that cannot evaluate must not silently permit — deny
132
- // with the spawn error so the user can fix the environment, not guess.
133
- deny(
134
- GATE_ID,
135
- `Could not run the lint command ("${lintCommand}"): ${result.error.message}. Fix ` +
136
- 'the lint setup or set lintCommand/blockCommitWithFailingLint in .ai/config.json.',
137
- );
138
- }
139
-
140
- if (result.status !== 0) {
141
- const combinedOutput = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
142
- deny(
143
- GATE_ID,
144
- `Lint failed (exit ${result.status}) — commit blocked until it passes:\n` +
145
- `${tailLines(combinedOutput, OUTPUT_TAIL_LINES)}`,
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
+ );