@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,88 +1,110 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { runGate, deny, TOOL_GROUPS } from '../../lib/hook-io.mjs';
|
|
3
|
-
|
|
4
|
-
const GATE_ID = 'audit-before-build';
|
|
5
|
-
const CONFIG_KEY = 'requireAuditBeforeBuilding';
|
|
6
|
-
|
|
7
|
-
const DEFAULT_EXECUTABLE_EXTENSIONS = [
|
|
8
|
-
'.js',
|
|
9
|
-
'.mjs',
|
|
10
|
-
'.cjs',
|
|
11
|
-
'.ts',
|
|
12
|
-
'.py',
|
|
13
|
-
'.sh',
|
|
14
|
-
'.ps1',
|
|
15
|
-
];
|
|
16
|
-
const DEFAULT_TOOL_FOLDERS = ['scripts/', 'hooks/', 'tools/'];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
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
|
-
)
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { runGate, deny, TOOL_GROUPS } from '../../lib/hook-io.mjs';
|
|
3
|
+
|
|
4
|
+
const GATE_ID = 'audit-before-build';
|
|
5
|
+
const CONFIG_KEY = 'requireAuditBeforeBuilding';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EXECUTABLE_EXTENSIONS = [
|
|
8
|
+
'.js',
|
|
9
|
+
'.mjs',
|
|
10
|
+
'.cjs',
|
|
11
|
+
'.ts',
|
|
12
|
+
'.py',
|
|
13
|
+
'.sh',
|
|
14
|
+
'.ps1',
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_TOOL_FOLDERS = ['scripts/', 'hooks/', 'tools/'];
|
|
17
|
+
|
|
18
|
+
// A verb ("create", "escribir", ...) followed within 60 non-period characters by a tool noun
|
|
19
|
+
// ("script", "gate", ...) signals intent to build a new tool. Split into two smaller patterns
|
|
20
|
+
// (checked one after the other, within a bounded window) instead of one combined alternation:
|
|
21
|
+
// the single-regex form crossed sonarjs's regex-complexity budget, and this reads easier too.
|
|
22
|
+
const NEW_TOOL_VERB_PATTERN =
|
|
23
|
+
/\b(?:create|write|build|add|cre[aá]r?|escrib(?:e|ir|í)|constru(?:ye|ir)|agreg[aá]r?)\b/giu;
|
|
24
|
+
const NEW_TOOL_NOUN_PATTERN =
|
|
25
|
+
/\b(?:script|verifier|checker|gate|hook|linter|tool|verificador|chequeador|herramienta)\b/iu;
|
|
26
|
+
const NEW_TOOL_NOUN_WINDOW = 60;
|
|
27
|
+
|
|
28
|
+
function hasNewToolIntent(text) {
|
|
29
|
+
NEW_TOOL_VERB_PATTERN.lastIndex = 0;
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = NEW_TOOL_VERB_PATTERN.exec(text)) !== null) {
|
|
32
|
+
const afterVerb = match.index + match[0].length;
|
|
33
|
+
let window = text.slice(afterVerb, afterVerb + NEW_TOOL_NOUN_WINDOW);
|
|
34
|
+
const dotIndex = window.indexOf('.');
|
|
35
|
+
if (dotIndex !== -1) window = window.slice(0, dotIndex);
|
|
36
|
+
if (NEW_TOOL_NOUN_PATTERN.test(window)) return true;
|
|
37
|
+
if (match.index === NEW_TOOL_VERB_PATTERN.lastIndex)
|
|
38
|
+
NEW_TOOL_VERB_PATTERN.lastIndex += 1;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const AUDIT_EVIDENCE_PATTERN =
|
|
44
|
+
/already exists|no existing tool|no plugin|audited and|justification:|no existe una herramienta|no existe la herramienta|no hay plugin|ya existe|busque? si ya existe|verifiqu[eé] que no (hay|existe)|audit[eé] herramientas|justificacion:|justificaci[oó]n:/i;
|
|
45
|
+
|
|
46
|
+
const INLINE_JUSTIFICATION_PATTERN =
|
|
47
|
+
/justification:|no-reinvent|audit:|justificacion:|justificaci[oó]n:/i;
|
|
48
|
+
|
|
49
|
+
function checkDelegation(toolInput) {
|
|
50
|
+
const prompt = toolInput?.prompt;
|
|
51
|
+
if (typeof prompt !== 'string') return;
|
|
52
|
+
if (!hasNewToolIntent(prompt)) return;
|
|
53
|
+
if (AUDIT_EVIDENCE_PATTERN.test(prompt)) return;
|
|
54
|
+
|
|
55
|
+
deny(
|
|
56
|
+
GATE_ID,
|
|
57
|
+
'Delegating creation of a new script/checker/gate/hook/linter/tool without evidence of a prior audit. State what you searched and why no existing tool covers this (e.g. "audited and no existing tool...").',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isNewToolFile(filePath, parameters) {
|
|
62
|
+
if (!filePath) return false;
|
|
63
|
+
const inToolFolder = parameters.toolFolders.some((folder) =>
|
|
64
|
+
filePath.includes(folder),
|
|
65
|
+
);
|
|
66
|
+
if (!inToolFolder) return false;
|
|
67
|
+
return parameters.executableExtensions.some((extension) =>
|
|
68
|
+
filePath.endsWith(extension),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkWrite(toolInput, parameters) {
|
|
73
|
+
const rawPath = toolInput?.file_path ?? '';
|
|
74
|
+
const filePath = rawPath.replace(/\\/g, '/');
|
|
75
|
+
if (!isNewToolFile(filePath, parameters)) return;
|
|
76
|
+
|
|
77
|
+
// Editing an EXISTING file is not building a new tool — only creation needs the audit.
|
|
78
|
+
// A file already on disk (any tool that carries file_path) is an edit, so it is allowed.
|
|
79
|
+
// This is what makes maintaining the gates themselves possible with the gates active.
|
|
80
|
+
if (rawPath && existsSync(rawPath)) return;
|
|
81
|
+
|
|
82
|
+
const content = toolInput?.content ?? toolInput?.new_string;
|
|
83
|
+
if (typeof content !== 'string') return;
|
|
84
|
+
if (INLINE_JUSTIFICATION_PATTERN.test(content)) return;
|
|
85
|
+
|
|
86
|
+
deny(
|
|
87
|
+
GATE_ID,
|
|
88
|
+
`Creating a new executable tool at ${filePath} without a justification comment (e.g. "justification: ..."). Document why no existing tool covers this before building a new one.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
runGate(
|
|
93
|
+
{
|
|
94
|
+
id: GATE_ID,
|
|
95
|
+
configKey: CONFIG_KEY,
|
|
96
|
+
enabledByDefault: false,
|
|
97
|
+
defaultParams: {
|
|
98
|
+
executableExtensions: DEFAULT_EXECUTABLE_EXTENSIONS,
|
|
99
|
+
toolFolders: DEFAULT_TOOL_FOLDERS,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
({ toolName, toolInput, parameters }) => {
|
|
103
|
+
if (TOOL_GROUPS.delegation.includes(toolName)) {
|
|
104
|
+
checkDelegation(toolInput);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!TOOL_GROUPS.write.includes(toolName)) return;
|
|
108
|
+
checkWrite(toolInput, parameters);
|
|
109
|
+
},
|
|
110
|
+
);
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
// autonomous-mode — when the project turns on autonomous mode, the assistant must stop
|
|
2
|
-
// asking and decide. This gate denies the AskUserQuestion tool while the mode is on, so a
|
|
3
|
-
// question popup cannot interrupt an unattended run. The assistant is expected to take the
|
|
4
|
-
// best, aligned decision and state the reversible assumption instead of asking.
|
|
5
|
-
//
|
|
6
|
-
// justification: no existing tool covers this. It is the deterministic half of "run without
|
|
7
|
-
// asking": a gate can block the question tool; the prose reminder (via the session-start
|
|
8
|
-
// hook and this gate's message) covers the rest, which a hook cannot force.
|
|
9
|
-
//
|
|
10
|
-
// ── When it acts ────────────────────────────────────────────────────────────────────
|
|
11
|
-
// Only when autonomousMode is enabled in .ai/config.json (project) or the global config.
|
|
12
|
-
// Off by default. The user is also reminded at session start that the mode is on (see the
|
|
13
|
-
// session hook), in case they forgot to turn it off.
|
|
14
|
-
//
|
|
15
|
-
// ── What it does NOT do ─────────────────────────────────────────────────────────────
|
|
16
|
-
// It blocks the AskUserQuestion TOOL. It cannot stop the assistant from asking in plain
|
|
17
|
-
// prose (no hook sees chat text) — the injected reminder pushes against that, but the only
|
|
18
|
-
// deterministic lever is the question tool, and this gate pulls it.
|
|
19
|
-
|
|
20
|
-
import { runGate, deny, toolInGroups } from '../../lib/hook-io.mjs';
|
|
21
|
-
|
|
22
|
-
const GATE_ID = 'autonomous-mode';
|
|
23
|
-
const CONFIG_KEY = 'autonomousMode';
|
|
24
|
-
|
|
25
|
-
// The tool groups whose members are "asking the user something". `toolInGroups` matches the
|
|
26
|
-
// native AskUserQuestion AND any MCP ask/confirm/elicit surface (mcp__*__ask_user_*), so an
|
|
27
|
-
// autonomous run can't be interrupted by a question routed through an MCP server either.
|
|
28
|
-
const QUESTION_GROUPS = ['question'];
|
|
29
|
-
|
|
30
|
-
const DENY_MESSAGE =
|
|
31
|
-
'Autonomous mode is ON for this project: do not ask the user, decide instead. The question ' +
|
|
32
|
-
'popup was intercepted and never shown. Now, in your VISIBLE reply, leave a one-line trace ' +
|
|
33
|
-
'so the user sees what was suppressed and what you chose — e.g. "[autonomous] I was going ' +
|
|
34
|
-
'to ask <the question>; I decided <choice>, assuming <reversible assumption>." Then take ' +
|
|
35
|
-
'that best, goal-aligned decision and proceed. Only a genuinely irreversible or dangerous ' +
|
|
36
|
-
'choice (deleting data, money, production) justifies stopping — and then stop in prose ' +
|
|
37
|
-
'saying exactly what and why, do not reach for the popup again. To let questions through, ' +
|
|
38
|
-
'set "autonomousMode": false in .ai/config.json.';
|
|
39
|
-
|
|
40
|
-
runGate(
|
|
41
|
-
{
|
|
42
|
-
id: GATE_ID,
|
|
43
|
-
configKey: CONFIG_KEY,
|
|
44
|
-
enabledByDefault: false,
|
|
45
|
-
},
|
|
46
|
-
({ toolName }) => {
|
|
47
|
-
if (!toolInGroups(toolName, QUESTION_GROUPS)) return;
|
|
48
|
-
deny(GATE_ID, DENY_MESSAGE);
|
|
49
|
-
},
|
|
50
|
-
);
|
|
1
|
+
// autonomous-mode — when the project turns on autonomous mode, the assistant must stop
|
|
2
|
+
// asking and decide. This gate denies the AskUserQuestion tool while the mode is on, so a
|
|
3
|
+
// question popup cannot interrupt an unattended run. The assistant is expected to take the
|
|
4
|
+
// best, aligned decision and state the reversible assumption instead of asking.
|
|
5
|
+
//
|
|
6
|
+
// justification: no existing tool covers this. It is the deterministic half of "run without
|
|
7
|
+
// asking": a gate can block the question tool; the prose reminder (via the session-start
|
|
8
|
+
// hook and this gate's message) covers the rest, which a hook cannot force.
|
|
9
|
+
//
|
|
10
|
+
// ── When it acts ────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Only when autonomousMode is enabled in .ai/config.json (project) or the global config.
|
|
12
|
+
// Off by default. The user is also reminded at session start that the mode is on (see the
|
|
13
|
+
// session hook), in case they forgot to turn it off.
|
|
14
|
+
//
|
|
15
|
+
// ── What it does NOT do ─────────────────────────────────────────────────────────────
|
|
16
|
+
// It blocks the AskUserQuestion TOOL. It cannot stop the assistant from asking in plain
|
|
17
|
+
// prose (no hook sees chat text) — the injected reminder pushes against that, but the only
|
|
18
|
+
// deterministic lever is the question tool, and this gate pulls it.
|
|
19
|
+
|
|
20
|
+
import { runGate, deny, toolInGroups } from '../../lib/hook-io.mjs';
|
|
21
|
+
|
|
22
|
+
const GATE_ID = 'autonomous-mode';
|
|
23
|
+
const CONFIG_KEY = 'autonomousMode';
|
|
24
|
+
|
|
25
|
+
// The tool groups whose members are "asking the user something". `toolInGroups` matches the
|
|
26
|
+
// native AskUserQuestion AND any MCP ask/confirm/elicit surface (mcp__*__ask_user_*), so an
|
|
27
|
+
// autonomous run can't be interrupted by a question routed through an MCP server either.
|
|
28
|
+
const QUESTION_GROUPS = ['question'];
|
|
29
|
+
|
|
30
|
+
const DENY_MESSAGE =
|
|
31
|
+
'Autonomous mode is ON for this project: do not ask the user, decide instead. The question ' +
|
|
32
|
+
'popup was intercepted and never shown. Now, in your VISIBLE reply, leave a one-line trace ' +
|
|
33
|
+
'so the user sees what was suppressed and what you chose — e.g. "[autonomous] I was going ' +
|
|
34
|
+
'to ask <the question>; I decided <choice>, assuming <reversible assumption>." Then take ' +
|
|
35
|
+
'that best, goal-aligned decision and proceed. Only a genuinely irreversible or dangerous ' +
|
|
36
|
+
'choice (deleting data, money, production) justifies stopping — and then stop in prose ' +
|
|
37
|
+
'saying exactly what and why, do not reach for the popup again. To let questions through, ' +
|
|
38
|
+
'set "autonomousMode": false in .ai/config.json.';
|
|
39
|
+
|
|
40
|
+
runGate(
|
|
41
|
+
{
|
|
42
|
+
id: GATE_ID,
|
|
43
|
+
configKey: CONFIG_KEY,
|
|
44
|
+
enabledByDefault: false,
|
|
45
|
+
},
|
|
46
|
+
({ toolName }) => {
|
|
47
|
+
if (!toolInGroups(toolName, QUESTION_GROUPS)) return;
|
|
48
|
+
deny(GATE_ID, DENY_MESSAGE);
|
|
49
|
+
},
|
|
50
|
+
);
|