@devrik-tools/claude-gates 0.1.2 → 0.2.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 +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// doctor — SessionStart hook. Validates the environment claude-gates needs and speaks
|
|
2
|
+
// ONLY on failure: node's version (node:sqlite and other built-ins the plugin relies on
|
|
3
|
+
// need >=22.5.0, read from this package's own engines.node when reachable, else a
|
|
4
|
+
// hard-coded fallback) and that every gate script the registry declares actually exists
|
|
5
|
+
// on disk (a plugin installed half — e.g. copied without a file, or a stale global
|
|
6
|
+
// install) fails silently otherwise: hooks.json references a script that is not there,
|
|
7
|
+
// and Claude Code just skips it without telling anyone).
|
|
8
|
+
//
|
|
9
|
+
// justification: no existing tool covers this. wiring-check.mjs (same family) checks
|
|
10
|
+
// registry vs hooks.json vs disk cross-consistency; doctor.mjs checks the runtime
|
|
11
|
+
// environment itself (node version) plus the same "script exists" fact from the angle
|
|
12
|
+
// of "can this plugin even run", so a broken environment is caught before any gate is
|
|
13
|
+
// invoked and blocks something for the wrong reason.
|
|
14
|
+
//
|
|
15
|
+
// Fail-safe: every check is wrapped so a doctor bug never blocks a session from
|
|
16
|
+
// starting — worst case it silently skips a check rather than throwing.
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const STDIN_FILE_DESCRIPTOR = 0;
|
|
24
|
+
const SESSION_START_EVENT = 'SessionStart';
|
|
25
|
+
const CONFIG_KEY = 'validateEnvironmentOnStart';
|
|
26
|
+
const DEFAULT_MIN_NODE_VERSION = '22.5.0';
|
|
27
|
+
|
|
28
|
+
const HOOKS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const REPOSITORY_ROOT = join(HOOKS_DIRECTORY, '..', '..', '..');
|
|
30
|
+
const PACKAGE_JSON_PATH = join(REPOSITORY_ROOT, 'package.json');
|
|
31
|
+
const REGISTRY_PATH = join(REPOSITORY_ROOT, 'registry.json');
|
|
32
|
+
|
|
33
|
+
// Same config lookup as the other session hooks (see session-tasks.mjs / register-requests.mjs):
|
|
34
|
+
// kept local and dependency-free so this plugin works standalone.
|
|
35
|
+
const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
|
|
36
|
+
const PROJECT_CONFIG = join('.ai', 'config.json');
|
|
37
|
+
const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
|
|
38
|
+
|
|
39
|
+
const BOM_CHAR_CODE = 0xfeff;
|
|
40
|
+
|
|
41
|
+
/** Strips a UTF-8 BOM a Windows editor/tool may have written before the JSON. */
|
|
42
|
+
function stripBom(text) {
|
|
43
|
+
return text.charCodeAt(0) === BOM_CHAR_CODE ? text.slice(1) : text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(path) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(stripBom(readFileSync(path, 'utf8')));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function projectRootOf(startDirectory) {
|
|
55
|
+
let current = startDirectory;
|
|
56
|
+
while (true) {
|
|
57
|
+
if (
|
|
58
|
+
PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
|
|
59
|
+
) {
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(current);
|
|
63
|
+
if (parent === current) return null;
|
|
64
|
+
current = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The doctor gate config entry (project overrides global), or null. */
|
|
69
|
+
function doctorConfig(startDirectory) {
|
|
70
|
+
const root = projectRootOf(startDirectory);
|
|
71
|
+
const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
|
|
72
|
+
const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
|
|
73
|
+
const layer = projectData?.gates ?? globalData?.gates ?? {};
|
|
74
|
+
const entry = layer[CONFIG_KEY];
|
|
75
|
+
if (typeof entry === 'boolean') return { enabled: entry };
|
|
76
|
+
if (entry && typeof entry === 'object') return entry;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readPayload() {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const VERSION_SEGMENT_COUNT = 3; // major, minor, patch
|
|
89
|
+
|
|
90
|
+
/** Parses "22.5.0" (or "v22.5.0") into [22, 5, 0]; null when unparseable. */
|
|
91
|
+
function parseVersion(raw) {
|
|
92
|
+
const withoutPrefix = String(raw ?? '').replace(/^v/, '');
|
|
93
|
+
const segments = withoutPrefix.split('.').slice(0, VERSION_SEGMENT_COUNT);
|
|
94
|
+
if (segments.length < VERSION_SEGMENT_COUNT) return null;
|
|
95
|
+
const numbers = segments.map((segment) => Number(/^\d+/.exec(segment)?.[0]));
|
|
96
|
+
return numbers.some((n) => Number.isNaN(n)) ? null : numbers;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True when `actual` >= `required`, comparing major.minor.patch lexicographically. */
|
|
100
|
+
function versionAtLeast(actual, required) {
|
|
101
|
+
const a = parseVersion(actual);
|
|
102
|
+
const r = parseVersion(required);
|
|
103
|
+
if (!a || !r) return true; // can't parse: don't fail the session over a doctor bug
|
|
104
|
+
for (let index = 0; index < VERSION_SEGMENT_COUNT; index += 1) {
|
|
105
|
+
if (a[index] > r[index]) return true;
|
|
106
|
+
if (a[index] < r[index]) return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The first "x.y.z"-shaped token inside an engines.node range string (">=22.5.0" -> "22.5.0"). */
|
|
112
|
+
function firstVersionToken(engineRange) {
|
|
113
|
+
for (const token of engineRange.split(/\s+/)) {
|
|
114
|
+
const digits = token.replace(/[^\d.]/g, '');
|
|
115
|
+
if (parseVersion(digits)) return digits;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The minimum node version this install requires: package.json's engines.node, else the fallback. */
|
|
121
|
+
function minNodeVersionRequired(configuredMin) {
|
|
122
|
+
if (configuredMin) return configuredMin;
|
|
123
|
+
const packageManifest = readJson(PACKAGE_JSON_PATH);
|
|
124
|
+
const engineRange = packageManifest?.engines?.node;
|
|
125
|
+
const token = engineRange ? firstVersionToken(engineRange) : null;
|
|
126
|
+
return token ?? DEFAULT_MIN_NODE_VERSION;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkNodeVersion(minVersion) {
|
|
130
|
+
if (versionAtLeast(process.version, minVersion)) return null;
|
|
131
|
+
return (
|
|
132
|
+
`node ${process.version} is below the required >=${minVersion}. ` +
|
|
133
|
+
'Some gates (e.g. node:sqlite-backed task tracking) will not work until node is upgraded.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Every script the registry's PreToolUse/Stop gates (folder-style: gates/<id>/index.mjs) declare. */
|
|
138
|
+
function missingFolderGateScripts() {
|
|
139
|
+
const registry = readJson(REGISTRY_PATH);
|
|
140
|
+
if (!registry?.families) return []; // can't read the registry: nothing to report
|
|
141
|
+
|
|
142
|
+
const problems = [];
|
|
143
|
+
for (const family of registry.families) {
|
|
144
|
+
const pluginName = family.plugin ?? 'gates';
|
|
145
|
+
const pluginHooksDirectory = join(
|
|
146
|
+
REPOSITORY_ROOT,
|
|
147
|
+
'plugins',
|
|
148
|
+
pluginName,
|
|
149
|
+
'hooks',
|
|
150
|
+
);
|
|
151
|
+
for (const gate of family.gates ?? []) {
|
|
152
|
+
const scriptPath = join(pluginHooksDirectory, gate.script);
|
|
153
|
+
if (!existsSync(scriptPath)) {
|
|
154
|
+
problems.push(
|
|
155
|
+
`${gate.id} (expected ${join('plugins', pluginName, 'hooks', gate.script)})`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return problems;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function speak(context) {
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
JSON.stringify({
|
|
166
|
+
hookSpecificOutput: {
|
|
167
|
+
hookEventName: SESSION_START_EVENT,
|
|
168
|
+
additionalContext: context,
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function main() {
|
|
175
|
+
const payload = readPayload();
|
|
176
|
+
const cwd = payload.cwd || process.cwd();
|
|
177
|
+
|
|
178
|
+
const config = doctorConfig(cwd);
|
|
179
|
+
if (config && config.enabled === false) return; // gate turned off for this project
|
|
180
|
+
|
|
181
|
+
const problems = [];
|
|
182
|
+
|
|
183
|
+
const minVersion = minNodeVersionRequired(config?.minNodeVersion);
|
|
184
|
+
const versionProblem = checkNodeVersion(minVersion);
|
|
185
|
+
if (versionProblem) problems.push(versionProblem);
|
|
186
|
+
|
|
187
|
+
const missingScripts = missingFolderGateScripts();
|
|
188
|
+
if (missingScripts.length > 0) {
|
|
189
|
+
problems.push(
|
|
190
|
+
`${missingScripts.length} gate script(s) declared in registry.json are missing on disk: ${missingScripts.join(
|
|
191
|
+
', ',
|
|
192
|
+
)}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (problems.length === 0) return; // environment is healthy: stay silent
|
|
197
|
+
|
|
198
|
+
speak(
|
|
199
|
+
`[doctor] claude-gates environment check failed:\n- ${problems.join('\n- ')}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
main();
|
|
205
|
+
} catch {
|
|
206
|
+
// A doctor bug must never block session start.
|
|
207
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
+
|
|
11
|
+
function runGate(payload, { config } = {}) {
|
|
12
|
+
const project = mkdtempSync(join(tmpdir(), 'audit-before-build-edge-'));
|
|
13
|
+
mkdirSync(join(project, '.git'));
|
|
14
|
+
if (config) {
|
|
15
|
+
mkdirSync(join(project, '.ai'));
|
|
16
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
+
}
|
|
18
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
+
input: JSON.stringify(payload),
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
cwd: project,
|
|
22
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
23
|
+
});
|
|
24
|
+
return { out: out.trim() ? JSON.parse(out.trim()) : null, project };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function delegate(prompt) {
|
|
28
|
+
return { tool_name: 'Agent', tool_input: { prompt } };
|
|
29
|
+
}
|
|
30
|
+
function write(filePath, content) {
|
|
31
|
+
return { tool_name: 'Write', tool_input: { file_path: filePath, content } };
|
|
32
|
+
}
|
|
33
|
+
function isDeny(result) {
|
|
34
|
+
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ENABLE = { config: { gates: { requireAuditBeforeBuilding: true } } };
|
|
38
|
+
|
|
39
|
+
// EDGE CASE (BUG): AUDIT_EVIDENCE_PATTERN is a bare substring/regex test over the WHOLE
|
|
40
|
+
// delegation prompt with no proximity/ordering requirement relative to the tool-creation
|
|
41
|
+
// intent. An unrelated, earlier sentence that happens to say "no existing tool" anywhere
|
|
42
|
+
// in a long prompt satisfies the audit-evidence check even when it has nothing to do with
|
|
43
|
+
// the actual tool being created, so a prompt can smuggle past the gate by mentioning
|
|
44
|
+
// boilerplate audit language irrelevantly.
|
|
45
|
+
test('BUG: false negative — audit language elsewhere in the prompt, unrelated to the new tool, satisfies the check', () => {
|
|
46
|
+
const prompt =
|
|
47
|
+
'Context: earlier we confirmed no existing tool handles our deploy pipeline. ' +
|
|
48
|
+
'Now, unrelated task: create a new script that scrapes user passwords from logs.';
|
|
49
|
+
const { out } = runGate(delegate(prompt), ENABLE);
|
|
50
|
+
assert.equal(out, null, 'gate allows because AUDIT_EVIDENCE_PATTERN matched irrelevant boilerplate anywhere in the prompt');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// EDGE CASE (BUG): INLINE_JUSTIFICATION_PATTERN for a direct Write is similarly a bare
|
|
54
|
+
// substring test over the whole file content. Any comment anywhere in the file containing
|
|
55
|
+
// the word "justification:" (even quoting/describing the concept, not actually justifying
|
|
56
|
+
// THIS file) passes.
|
|
57
|
+
test('BUG: false negative — the word "justification:" appearing in an unrelated docstring satisfies the check for any new tool file', () => {
|
|
58
|
+
const content = [
|
|
59
|
+
'// This module documents how other files use the phrase justification: <reason> in',
|
|
60
|
+
'// their headers, as an example of our commenting convention.',
|
|
61
|
+
'export function run() {}',
|
|
62
|
+
].join('\n');
|
|
63
|
+
const { out } = runGate(write('/repo/scripts/new-thing.mjs', content), ENABLE);
|
|
64
|
+
assert.equal(out, null, 'gate allows because the literal string "justification:" appears anywhere, regardless of whether it actually justifies this file');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// EDGE CASE: confirm the "editing an existing file" exemption (checkWrite, index.mjs line
|
|
68
|
+
// 56-58) can be trivially satisfied by ANY tool call whose file_path happens to already
|
|
69
|
+
// exist on disk from an unrelated prior write within the same directory — verifying this
|
|
70
|
+
// is intentional per the comment, not a further bug, but documenting the boundary.
|
|
71
|
+
test('OK (documented, not a new bug): pre-creating an empty file at the target path bypasses audit entirely on the next edit', () => {
|
|
72
|
+
const { project } = runGate(delegate('noop'), ENABLE); // just to get an isolated project dir
|
|
73
|
+
const targetPath = join(project, 'scripts', 'sneaky.mjs');
|
|
74
|
+
mkdirSync(join(project, 'scripts'), { recursive: true });
|
|
75
|
+
writeFileSync(targetPath, ''); // pre-create empty file outside the gate's view
|
|
76
|
+
const out2 = execFileSync(process.execPath, [GATE], {
|
|
77
|
+
input: JSON.stringify(write(targetPath, 'export function run() {}')),
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
cwd: project,
|
|
80
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
81
|
+
});
|
|
82
|
+
assert.equal(out2.trim() ? JSON.parse(out2.trim()) : null, null, 'existsSync(rawPath) exemption means pre-touching the file first defeats the whole gate');
|
|
83
|
+
});
|
|
@@ -1,88 +1,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
|
-
const NEW_TOOL_INTENT_PATTERN =
|
|
19
|
-
/\b(create|write|build|add)\b[^.]{0,60}\b(script|verifier|checker|gate|hook|linter|tool)\b/
|
|
20
|
-
|
|
21
|
-
const AUDIT_EVIDENCE_PATTERN =
|
|
22
|
-
/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;
|
|
23
|
-
|
|
24
|
-
const INLINE_JUSTIFICATION_PATTERN =
|
|
25
|
-
/justification:|no-reinvent|audit:|justificacion:|justificaci[oó]n:/i;
|
|
26
|
-
|
|
27
|
-
function checkDelegation(toolInput) {
|
|
28
|
-
const prompt = toolInput?.prompt;
|
|
29
|
-
if (typeof prompt !== 'string') return;
|
|
30
|
-
if (!NEW_TOOL_INTENT_PATTERN.test(prompt)) return;
|
|
31
|
-
if (AUDIT_EVIDENCE_PATTERN.test(prompt)) return;
|
|
32
|
-
|
|
33
|
-
deny(
|
|
34
|
-
GATE_ID,
|
|
35
|
-
'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...").',
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isNewToolFile(filePath, parameters) {
|
|
40
|
-
if (!filePath) return false;
|
|
41
|
-
const inToolFolder = parameters.toolFolders.some((folder) =>
|
|
42
|
-
filePath.includes(folder),
|
|
43
|
-
);
|
|
44
|
-
if (!inToolFolder) return false;
|
|
45
|
-
return parameters.executableExtensions.some((extension) =>
|
|
46
|
-
filePath.endsWith(extension),
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function checkWrite(toolInput, parameters) {
|
|
51
|
-
const rawPath = toolInput?.file_path ?? '';
|
|
52
|
-
const filePath = rawPath.replace(/\\/g, '/');
|
|
53
|
-
if (!isNewToolFile(filePath, parameters)) return;
|
|
54
|
-
|
|
55
|
-
// Editing an EXISTING file is not building a new tool — only creation needs the audit.
|
|
56
|
-
// A file already on disk (any tool that carries file_path) is an edit, so it is allowed.
|
|
57
|
-
// This is what makes maintaining the gates themselves possible with the gates active.
|
|
58
|
-
if (rawPath && existsSync(rawPath)) return;
|
|
59
|
-
|
|
60
|
-
const content = toolInput?.content ?? toolInput?.new_string;
|
|
61
|
-
if (typeof content !== 'string') return;
|
|
62
|
-
if (INLINE_JUSTIFICATION_PATTERN.test(content)) return;
|
|
63
|
-
|
|
64
|
-
deny(
|
|
65
|
-
GATE_ID,
|
|
66
|
-
`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.`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
runGate(
|
|
71
|
-
{
|
|
72
|
-
id: GATE_ID,
|
|
73
|
-
configKey: CONFIG_KEY,
|
|
74
|
-
enabledByDefault: false,
|
|
75
|
-
defaultParams: {
|
|
76
|
-
executableExtensions: DEFAULT_EXECUTABLE_EXTENSIONS,
|
|
77
|
-
toolFolders: DEFAULT_TOOL_FOLDERS,
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
({ toolName, toolInput, parameters }) => {
|
|
81
|
-
if (TOOL_GROUPS.delegation.includes(toolName)) {
|
|
82
|
-
checkDelegation(toolInput);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
if (!TOOL_GROUPS.write.includes(toolName)) return;
|
|
86
|
-
checkWrite(toolInput, parameters);
|
|
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
|
+
const NEW_TOOL_INTENT_PATTERN =
|
|
19
|
+
/\b(create|write|build|add|cre(?:a|á)|crear|escrib(?:e|í)|escribir|constru(?:ye|í)|construir|agreg(?:a|á)|agregar)\b[^.]{0,60}\b(script|verifier|checker|gate|hook|linter|tool|verificador|chequeador|gate|hook|linter|herramienta)\b/iu;
|
|
20
|
+
|
|
21
|
+
const AUDIT_EVIDENCE_PATTERN =
|
|
22
|
+
/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;
|
|
23
|
+
|
|
24
|
+
const INLINE_JUSTIFICATION_PATTERN =
|
|
25
|
+
/justification:|no-reinvent|audit:|justificacion:|justificaci[oó]n:/i;
|
|
26
|
+
|
|
27
|
+
function checkDelegation(toolInput) {
|
|
28
|
+
const prompt = toolInput?.prompt;
|
|
29
|
+
if (typeof prompt !== 'string') return;
|
|
30
|
+
if (!NEW_TOOL_INTENT_PATTERN.test(prompt)) return;
|
|
31
|
+
if (AUDIT_EVIDENCE_PATTERN.test(prompt)) return;
|
|
32
|
+
|
|
33
|
+
deny(
|
|
34
|
+
GATE_ID,
|
|
35
|
+
'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...").',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isNewToolFile(filePath, parameters) {
|
|
40
|
+
if (!filePath) return false;
|
|
41
|
+
const inToolFolder = parameters.toolFolders.some((folder) =>
|
|
42
|
+
filePath.includes(folder),
|
|
43
|
+
);
|
|
44
|
+
if (!inToolFolder) return false;
|
|
45
|
+
return parameters.executableExtensions.some((extension) =>
|
|
46
|
+
filePath.endsWith(extension),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function checkWrite(toolInput, parameters) {
|
|
51
|
+
const rawPath = toolInput?.file_path ?? '';
|
|
52
|
+
const filePath = rawPath.replace(/\\/g, '/');
|
|
53
|
+
if (!isNewToolFile(filePath, parameters)) return;
|
|
54
|
+
|
|
55
|
+
// Editing an EXISTING file is not building a new tool — only creation needs the audit.
|
|
56
|
+
// A file already on disk (any tool that carries file_path) is an edit, so it is allowed.
|
|
57
|
+
// This is what makes maintaining the gates themselves possible with the gates active.
|
|
58
|
+
if (rawPath && existsSync(rawPath)) return;
|
|
59
|
+
|
|
60
|
+
const content = toolInput?.content ?? toolInput?.new_string;
|
|
61
|
+
if (typeof content !== 'string') return;
|
|
62
|
+
if (INLINE_JUSTIFICATION_PATTERN.test(content)) return;
|
|
63
|
+
|
|
64
|
+
deny(
|
|
65
|
+
GATE_ID,
|
|
66
|
+
`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.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
runGate(
|
|
71
|
+
{
|
|
72
|
+
id: GATE_ID,
|
|
73
|
+
configKey: CONFIG_KEY,
|
|
74
|
+
enabledByDefault: false,
|
|
75
|
+
defaultParams: {
|
|
76
|
+
executableExtensions: DEFAULT_EXECUTABLE_EXTENSIONS,
|
|
77
|
+
toolFolders: DEFAULT_TOOL_FOLDERS,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
({ toolName, toolInput, parameters }) => {
|
|
81
|
+
if (TOOL_GROUPS.delegation.includes(toolName)) {
|
|
82
|
+
checkDelegation(toolInput);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!TOOL_GROUPS.write.includes(toolName)) return;
|
|
86
|
+
checkWrite(toolInput, parameters);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
@@ -1,92 +1,117 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'audit-before-build-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
23
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
24
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
-
});
|
|
26
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function delegate(prompt) {
|
|
30
|
-
return { tool_name: 'Agent', tool_input: { prompt } };
|
|
31
|
-
}
|
|
32
|
-
function write(filePath, content) {
|
|
33
|
-
return { tool_name: 'Write', tool_input: { file_path: filePath, content } };
|
|
34
|
-
}
|
|
35
|
-
function isDeny(result) {
|
|
36
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const ENABLE = { config: { gates: { requireAuditBeforeBuilding: true } } };
|
|
40
|
-
|
|
41
|
-
test('denies delegation to create a new tool without audit evidence', () => {
|
|
42
|
-
assert.ok(
|
|
43
|
-
isDeny(runGate(delegate('create a new script that checks X'), ENABLE)),
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('allows delegation with audit evidence stated', () => {
|
|
48
|
-
assert.equal(
|
|
49
|
-
runGate(
|
|
50
|
-
delegate(
|
|
51
|
-
'create a new script that checks X. audited and no existing tool covers this.',
|
|
52
|
-
),
|
|
53
|
-
ENABLE,
|
|
54
|
-
),
|
|
55
|
-
null,
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('denies writing a new executable tool file without a justification comment', () => {
|
|
60
|
-
assert.equal(
|
|
61
|
-
runGate(
|
|
62
|
-
write('/repo/scripts/check-thing.mjs', 'export function run() {}'),
|
|
63
|
-
ENABLE,
|
|
64
|
-
)?.hookSpecificOutput?.permissionDecision,
|
|
65
|
-
'deny',
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('allows writing a tool file with a justification comment', () => {
|
|
70
|
-
assert.equal(
|
|
71
|
-
runGate(
|
|
72
|
-
write(
|
|
73
|
-
'/repo/scripts/check-thing.mjs',
|
|
74
|
-
'// justification: no existing tool does this\nexport function run() {}',
|
|
75
|
-
),
|
|
76
|
-
ENABLE,
|
|
77
|
-
),
|
|
78
|
-
null,
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('allows a write outside tool folders or non-executable extension', () => {
|
|
83
|
-
assert.equal(
|
|
84
|
-
runGate(write('/repo/docs/notes.md', 'plain notes'), ENABLE),
|
|
85
|
-
null,
|
|
86
|
-
);
|
|
87
|
-
assert.equal(runGate(write('/repo/scripts/data.json', '{}'), ENABLE), null);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('
|
|
91
|
-
assert.
|
|
92
|
-
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
+
|
|
11
|
+
function runGate(payload, { config } = {}) {
|
|
12
|
+
const project = mkdtempSync(join(tmpdir(), 'audit-before-build-'));
|
|
13
|
+
mkdirSync(join(project, '.git'));
|
|
14
|
+
if (config) {
|
|
15
|
+
mkdirSync(join(project, '.ai'));
|
|
16
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
+
}
|
|
18
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
+
input: JSON.stringify(payload),
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
cwd: project,
|
|
22
|
+
// Isolate from the user's real global config: point homedir() at the temp
|
|
23
|
+
// project so the global-config fallback finds nothing (registry default).
|
|
24
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
+
});
|
|
26
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function delegate(prompt) {
|
|
30
|
+
return { tool_name: 'Agent', tool_input: { prompt } };
|
|
31
|
+
}
|
|
32
|
+
function write(filePath, content) {
|
|
33
|
+
return { tool_name: 'Write', tool_input: { file_path: filePath, content } };
|
|
34
|
+
}
|
|
35
|
+
function isDeny(result) {
|
|
36
|
+
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ENABLE = { config: { gates: { requireAuditBeforeBuilding: true } } };
|
|
40
|
+
|
|
41
|
+
test('denies delegation to create a new tool without audit evidence', () => {
|
|
42
|
+
assert.ok(
|
|
43
|
+
isDeny(runGate(delegate('create a new script that checks X'), ENABLE)),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('allows delegation with audit evidence stated', () => {
|
|
48
|
+
assert.equal(
|
|
49
|
+
runGate(
|
|
50
|
+
delegate(
|
|
51
|
+
'create a new script that checks X. audited and no existing tool covers this.',
|
|
52
|
+
),
|
|
53
|
+
ENABLE,
|
|
54
|
+
),
|
|
55
|
+
null,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('denies writing a new executable tool file without a justification comment', () => {
|
|
60
|
+
assert.equal(
|
|
61
|
+
runGate(
|
|
62
|
+
write('/repo/scripts/check-thing.mjs', 'export function run() {}'),
|
|
63
|
+
ENABLE,
|
|
64
|
+
)?.hookSpecificOutput?.permissionDecision,
|
|
65
|
+
'deny',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('allows writing a tool file with a justification comment', () => {
|
|
70
|
+
assert.equal(
|
|
71
|
+
runGate(
|
|
72
|
+
write(
|
|
73
|
+
'/repo/scripts/check-thing.mjs',
|
|
74
|
+
'// justification: no existing tool does this\nexport function run() {}',
|
|
75
|
+
),
|
|
76
|
+
ENABLE,
|
|
77
|
+
),
|
|
78
|
+
null,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('allows a write outside tool folders or non-executable extension', () => {
|
|
83
|
+
assert.equal(
|
|
84
|
+
runGate(write('/repo/docs/notes.md', 'plain notes'), ENABLE),
|
|
85
|
+
null,
|
|
86
|
+
);
|
|
87
|
+
assert.equal(runGate(write('/repo/scripts/data.json', '{}'), ENABLE), null);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('denies delegation to create a new tool without audit evidence, in Spanish', () => {
|
|
91
|
+
assert.ok(
|
|
92
|
+
isDeny(runGate(delegate('crea un nuevo script que verifique X'), ENABLE)),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('bilingual control: ES and EN equivalent new-tool delegations both deny without audit evidence', () => {
|
|
97
|
+
const es = delegate('crea un nuevo script que verifique X');
|
|
98
|
+
const en = delegate('create a new script that checks X');
|
|
99
|
+
assert.ok(isDeny(runGate(es, ENABLE)));
|
|
100
|
+
assert.ok(isDeny(runGate(en, ENABLE)));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('allows a Spanish delegation with audit evidence stated', () => {
|
|
104
|
+
assert.equal(
|
|
105
|
+
runGate(
|
|
106
|
+
delegate(
|
|
107
|
+
'crea un nuevo script que verifique X. audite y no existe una herramienta que cubra esto.',
|
|
108
|
+
),
|
|
109
|
+
ENABLE,
|
|
110
|
+
),
|
|
111
|
+
null,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('disabled by default (registry default is false)', () => {
|
|
116
|
+
assert.equal(runGate(delegate('create a new script that checks X')), null);
|
|
117
|
+
});
|