critical-gate 2.3.1 → 2.4.1
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/README.md +44 -35
- package/dist/agent-onboarding/agent-instructions.d.ts.map +1 -1
- package/dist/agent-onboarding/agent-instructions.js +2 -2
- package/dist/agent-onboarding/agent-instructions.js.map +1 -1
- package/dist/cli/args.d.ts +57 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +209 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +170 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/entrypoint.d.ts +2 -0
- package/dist/cli/entrypoint.d.ts.map +1 -0
- package/dist/cli/entrypoint.js +18 -0
- package/dist/cli/entrypoint.js.map +1 -0
- package/dist/cli/git-hooks.d.ts +2 -0
- package/dist/cli/git-hooks.d.ts.map +1 -0
- package/dist/cli/git-hooks.js +31 -0
- package/dist/cli/git-hooks.js.map +1 -0
- package/dist/cli/help.d.ts +4 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +151 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/io.d.ts +3 -0
- package/dist/cli/io.d.ts.map +1 -0
- package/dist/cli/io.js +16 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +76 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/result.d.ts +4 -0
- package/dist/cli/result.d.ts.map +1 -0
- package/dist/cli/result.js +107 -0
- package/dist/cli/result.js.map +1 -0
- package/dist/cli/types.d.ts +30 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +7 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli.d.ts +5 -18
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -759
- package/dist/cli.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -1,763 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
export const ExitCode = {
|
|
9
|
-
Pass: 0,
|
|
10
|
-
FindingsFailed: 1,
|
|
11
|
-
UsageError: 2,
|
|
12
|
-
InternalError: 3
|
|
13
|
-
};
|
|
14
|
-
const defaultIo = {
|
|
15
|
-
stdout: (message) => console.log(message),
|
|
16
|
-
stderr: (message) => console.error(message),
|
|
17
|
-
writeFile: (path, content) => {
|
|
18
|
-
mkdirSync(getApiSnapshotOutputDirectory(path), { recursive: true });
|
|
19
|
-
writeFileSync(path, content, "utf8");
|
|
20
|
-
},
|
|
21
|
-
now: () => new Date(),
|
|
22
|
-
exists: (path) => existsSync(path),
|
|
23
|
-
readFile: (path) => readFileSync(path, "utf8"),
|
|
24
|
-
chmodFile: (path, mode) => chmodSync(path, mode),
|
|
25
|
-
readDiff: (baseRef, options) => readGitDiff({ baseRef, staged: options?.staged })
|
|
26
|
-
};
|
|
27
|
-
export function main(argv = process.argv.slice(2), io = defaultIo) {
|
|
28
|
-
try {
|
|
29
|
-
return runCli(argv, io);
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
const message = error instanceof Error ? error.message : "Unknown internal error";
|
|
33
|
-
io.stderr(`critical-gate: ${message}`);
|
|
34
|
-
return ExitCode.InternalError;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function runCli(argv, io) {
|
|
38
|
-
if (argv.includes("--version")) {
|
|
39
|
-
io.stdout(`critical-gate ${CLI_VERSION}`);
|
|
40
|
-
return ExitCode.Pass;
|
|
41
|
-
}
|
|
42
|
-
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
43
|
-
io.stdout(getHelpText());
|
|
44
|
-
return ExitCode.Pass;
|
|
45
|
-
}
|
|
46
|
-
const [command = "check", ...args] = argv;
|
|
47
|
-
if (!isCommandName(command)) {
|
|
48
|
-
io.stderr(`Unknown command: ${command}`);
|
|
49
|
-
io.stderr("Run critical-gate --help for usage.");
|
|
50
|
-
return ExitCode.UsageError;
|
|
51
|
-
}
|
|
52
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
53
|
-
io.stdout(getCommandHelpText(command));
|
|
54
|
-
return ExitCode.Pass;
|
|
55
|
-
}
|
|
56
|
-
if (command === "accept") {
|
|
57
|
-
return runAcceptCommand(args, io);
|
|
58
|
-
}
|
|
59
|
-
if (command === "teach") {
|
|
60
|
-
return runTeachCommand(args, io);
|
|
61
|
-
}
|
|
62
|
-
if (command === "snapshot-api") {
|
|
63
|
-
return runSnapshotApiCommand(args, io);
|
|
64
|
-
}
|
|
65
|
-
if (command === "install-hooks") {
|
|
66
|
-
return runInstallHooksCommand(args, io);
|
|
67
|
-
}
|
|
68
|
-
if (command === "init-policy") {
|
|
69
|
-
return runInitPolicyCommand(args, io);
|
|
70
|
-
}
|
|
71
|
-
if (command === "init-agent") {
|
|
72
|
-
return runInitAgentCommand(args, io);
|
|
73
|
-
}
|
|
74
|
-
const parsed = parseCheckArgs(args, command);
|
|
75
|
-
if (!parsed.ok) {
|
|
76
|
-
io.stderr(parsed.error);
|
|
77
|
-
io.stderr("Run critical-gate check --help for usage.");
|
|
78
|
-
return ExitCode.UsageError;
|
|
79
|
-
}
|
|
80
|
-
const diff = io.readDiff(parsed.options.base, { staged: parsed.options.staged });
|
|
81
|
-
const result = createGateResult(parsed.options, io.now(), diff, io);
|
|
82
|
-
const rendered = command === "hook"
|
|
83
|
-
? renderReport(result.summary.decision === "pass" ? { ...result, findings: [] } : result, "repair")
|
|
84
|
-
: renderReport(result, parsed.options.format);
|
|
85
|
-
if (parsed.options.output !== undefined) {
|
|
86
|
-
io.writeFile(parsed.options.output, rendered);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
io.stdout(rendered);
|
|
90
|
-
}
|
|
91
|
-
return result.summary.decision === "fail" ? ExitCode.FindingsFailed : ExitCode.Pass;
|
|
92
|
-
}
|
|
93
|
-
function parseCheckArgs(args, command) {
|
|
94
|
-
const options = {
|
|
95
|
-
format: "markdown",
|
|
96
|
-
strict: false,
|
|
97
|
-
staged: false
|
|
98
|
-
};
|
|
99
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
100
|
-
const arg = args[index];
|
|
101
|
-
if (arg === "--strict") {
|
|
102
|
-
options.strict = true;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (arg === "--staged") {
|
|
106
|
-
options.staged = true;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (arg === "--task" ||
|
|
110
|
-
arg === "--base" ||
|
|
111
|
-
arg === "--format" ||
|
|
112
|
-
arg === "--output" ||
|
|
113
|
-
arg === "--fail-on") {
|
|
114
|
-
const value = args[index + 1];
|
|
115
|
-
if (value === undefined || value.startsWith("--")) {
|
|
116
|
-
return { ok: false, error: `Missing value for ${arg}.` };
|
|
117
|
-
}
|
|
118
|
-
index += 1;
|
|
119
|
-
if (arg === "--task") {
|
|
120
|
-
options.task = value;
|
|
121
|
-
}
|
|
122
|
-
else if (arg === "--base") {
|
|
123
|
-
options.base = value;
|
|
124
|
-
}
|
|
125
|
-
else if (arg === "--format") {
|
|
126
|
-
if (!isReportFormat(value)) {
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
error: "Invalid --format value. Expected json, markdown, sarif, repair, or pr-comment."
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
options.format = value;
|
|
133
|
-
}
|
|
134
|
-
else if (arg === "--fail-on") {
|
|
135
|
-
if (!isFailOnSeverity(value)) {
|
|
136
|
-
return {
|
|
137
|
-
ok: false,
|
|
138
|
-
error: "Invalid --fail-on value. Expected blocker, high, or medium."
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
options.failOn = value;
|
|
142
|
-
}
|
|
143
|
-
else if (arg === "--output") {
|
|
144
|
-
options.output = value;
|
|
145
|
-
}
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
return { ok: false, error: `Unknown option: ${arg}` };
|
|
149
|
-
}
|
|
150
|
-
if (options.task === undefined || options.task.trim().length === 0) {
|
|
151
|
-
if (command === "hook") {
|
|
152
|
-
options.task = "Codex completed feature implementation";
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
return { ok: false, error: "Missing required --task value." };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (options.task.trim().length === 0) {
|
|
159
|
-
return { ok: false, error: "Missing required --task value." };
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
ok: true,
|
|
163
|
-
options: {
|
|
164
|
-
task: options.task,
|
|
165
|
-
base: options.base,
|
|
166
|
-
format: options.format ?? "markdown",
|
|
167
|
-
strict: options.strict ?? false,
|
|
168
|
-
staged: options.staged ?? false,
|
|
169
|
-
failOn: options.failOn,
|
|
170
|
-
output: options.output
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
function createGateResult(options, generatedAt, diffResult, io = {}) {
|
|
175
|
-
const configResult = loadCriticalGateConfig(diffResult.root, {
|
|
176
|
-
exists: io.exists,
|
|
177
|
-
readFile: io.readFile
|
|
178
|
-
});
|
|
179
|
-
const task = {
|
|
180
|
-
source: "cli",
|
|
181
|
-
text: options.task
|
|
182
|
-
};
|
|
183
|
-
const diff = {
|
|
184
|
-
baseRef: diffResult.baseRef,
|
|
185
|
-
headRef: diffResult.headRef,
|
|
186
|
-
files: diffResult.files
|
|
187
|
-
};
|
|
188
|
-
const packageJson = readOptionalPackageJson(diffResult.root, io);
|
|
189
|
-
const frameworkPacks = detectFrameworkPacks({
|
|
190
|
-
files: diff.files,
|
|
191
|
-
packageJson,
|
|
192
|
-
config: configResult.config
|
|
193
|
-
});
|
|
194
|
-
const monorepo = detectMonorepoContext(diffResult.root, diff.files, io);
|
|
195
|
-
const repositoryTokenIndex = buildRepositoryTokenIndex({
|
|
196
|
-
files: diff.files,
|
|
197
|
-
packageJson
|
|
198
|
-
});
|
|
199
|
-
const publicApiEntrypoints = resolvePublicApiEntrypoints(diffResult.root, io, getConfiguredPublicApiEntrypoints(configResult.config));
|
|
200
|
-
const publicApiEntrypointContext = publicApiEntrypoints.length > 0 ? publicApiEntrypoints : undefined;
|
|
201
|
-
const apiSnapshot = loadApiSurfaceSnapshot(diffResult.root, io);
|
|
202
|
-
const context = {
|
|
203
|
-
root: diffResult.root,
|
|
204
|
-
packageManager: "pnpm",
|
|
205
|
-
monorepo,
|
|
206
|
-
apiSnapshot: summarizeApiSurfaceSnapshot(apiSnapshot),
|
|
207
|
-
publicEntrypoints: publicApiEntrypointContext?.map((entrypoint) => entrypoint.path),
|
|
208
|
-
publicApiEntrypoints: publicApiEntrypointContext,
|
|
209
|
-
frameworkPacks: frameworkPacks.map((pack) => pack.id),
|
|
210
|
-
repositoryProfile: diffResult.repositoryProfile,
|
|
211
|
-
utilityIndex: diffResult.utilityIndex,
|
|
212
|
-
repositoryTokenIndex,
|
|
213
|
-
git: {
|
|
214
|
-
baseRef: diffResult.baseRef,
|
|
215
|
-
headRef: diffResult.headRef
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
const detectorContext = {
|
|
219
|
-
...context,
|
|
220
|
-
apiSurfaceSnapshot: apiSnapshot,
|
|
221
|
-
publicApiEntrypoints: publicApiEntrypointContext,
|
|
222
|
-
knowledge: diffResult.knowledge
|
|
223
|
-
};
|
|
224
|
-
const detectorFindings = runDetectors(task, diff, detectorContext);
|
|
225
|
-
const learningResult = applyLearningPolicy(detectorFindings, diff.files, {
|
|
226
|
-
...configResult.config.learning,
|
|
227
|
-
expectedSupportFiles: getConfiguredExpectedSupportFiles(configResult.config)
|
|
228
|
-
});
|
|
229
|
-
const findings = learningResult.findings;
|
|
230
|
-
const loadedHistoryIndex = diffResult.knowledge?.getLoadedHistoryIndex?.();
|
|
231
|
-
const loadedSolutionIndex = diffResult.knowledge?.getLoadedSolutionIndex?.();
|
|
232
|
-
context.repositoryProfile ??= loadedHistoryIndex?.profile;
|
|
233
|
-
context.utilityIndex ??= loadedSolutionIndex?.utilityIndex;
|
|
234
|
-
return {
|
|
235
|
-
schemaVersion: GATE_RESULT_SCHEMA_VERSION,
|
|
236
|
-
generatedAt: generatedAt.toISOString(),
|
|
237
|
-
task,
|
|
238
|
-
diff,
|
|
239
|
-
context,
|
|
240
|
-
findings,
|
|
241
|
-
summary: summarizeFindings(findings, task, diff, {
|
|
242
|
-
observationDetectors: getPolicyObservationDetectors(configResult.config),
|
|
243
|
-
blockingDetectors: getPolicyBlockingDetectors(configResult.config),
|
|
244
|
-
failOn: options.failOn ?? getConfiguredFailOn(configResult.config),
|
|
245
|
-
acceptedFindingIds: learningResult.appliedAcceptedFindings
|
|
246
|
-
}),
|
|
247
|
-
intentVerification: summarizeIntentVerification(task, diff.files),
|
|
248
|
-
intentQuality: analyzeTaskIntentQuality(task),
|
|
249
|
-
metadata: {
|
|
250
|
-
cliVersion: CLI_VERSION,
|
|
251
|
-
strict: options.strict,
|
|
252
|
-
staged: options.staged,
|
|
253
|
-
failOn: options.failOn ?? getConfiguredFailOn(configResult.config) ?? "high",
|
|
254
|
-
rolloutPolicy: configResult.config.rollout,
|
|
255
|
-
policy: configResult.config.policy,
|
|
256
|
-
frameworkPacks: frameworkPacks.map((pack) => pack.id),
|
|
257
|
-
learning: {
|
|
258
|
-
acceptedFindingsApplied: learningResult.appliedAcceptedFindings,
|
|
259
|
-
expectedSupportRulesApplied: learningResult.appliedExpectedSupportRules
|
|
260
|
-
},
|
|
261
|
-
configWarnings: configResult.warnings
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
function runSnapshotApiCommand(args, io) {
|
|
266
|
-
const parsed = parseSnapshotApiArgs(args);
|
|
267
|
-
if (!parsed.ok) {
|
|
268
|
-
io.stderr(parsed.error);
|
|
269
|
-
io.stderr("Run critical-gate snapshot-api --help for usage.");
|
|
270
|
-
return ExitCode.UsageError;
|
|
271
|
-
}
|
|
272
|
-
const root = io.readDiff().root;
|
|
273
|
-
const configResult = loadCriticalGateConfig(root, {
|
|
274
|
-
exists: io.exists,
|
|
275
|
-
readFile: io.readFile
|
|
276
|
-
});
|
|
277
|
-
const snapshot = buildApiSurfaceSnapshot({
|
|
278
|
-
root,
|
|
279
|
-
generatedAt: io.now(),
|
|
280
|
-
entrypoints: parsed.options.entrypoints,
|
|
281
|
-
policyEntrypoints: getConfiguredPublicApiEntrypoints(configResult.config),
|
|
282
|
-
reader: io
|
|
283
|
-
});
|
|
284
|
-
const outputPath = getApiSnapshotOutputPath(root, parsed.options.output);
|
|
285
|
-
io.writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
286
|
-
io.stdout(`Wrote public API snapshot to ${outputPath} (${snapshot.exports.length} exports across ${snapshot.entrypoints.length} entrypoints).`);
|
|
287
|
-
return ExitCode.Pass;
|
|
288
|
-
}
|
|
289
|
-
function runInstallHooksCommand(args, io) {
|
|
290
|
-
const parsed = parseInstallHooksArgs(args);
|
|
291
|
-
if (!parsed.ok) {
|
|
292
|
-
io.stderr(parsed.error);
|
|
293
|
-
io.stderr("Run critical-gate install-hooks --help for usage.");
|
|
294
|
-
return ExitCode.UsageError;
|
|
295
|
-
}
|
|
296
|
-
const root = io.readDiff().root;
|
|
297
|
-
const hooks = parsed.options.hook === "all" ? ["pre-commit", "pre-push"] : [parsed.options.hook];
|
|
298
|
-
const installed = [];
|
|
299
|
-
for (const hook of hooks) {
|
|
300
|
-
const path = join(root, ".git", "hooks", hook);
|
|
301
|
-
if (io.exists?.(path) === true && parsed.options.force !== true) {
|
|
302
|
-
io.stderr(`Refusing to overwrite existing ${hook} hook at ${path}. Re-run with --force.`);
|
|
303
|
-
return ExitCode.UsageError;
|
|
304
|
-
}
|
|
305
|
-
io.writeFile(path, renderGitHookScript(hook, parsed.options.cli));
|
|
306
|
-
io.chmodFile?.(path, 0o755);
|
|
307
|
-
installed.push(path);
|
|
308
|
-
}
|
|
309
|
-
io.stdout(`Installed Critical Gate hook(s): ${installed.join(", ")}`);
|
|
310
|
-
return ExitCode.Pass;
|
|
311
|
-
}
|
|
312
|
-
function runInitPolicyCommand(args, io) {
|
|
313
|
-
const parsed = parseInitPolicyArgs(args);
|
|
314
|
-
if (!parsed.ok) {
|
|
315
|
-
io.stderr(parsed.error);
|
|
316
|
-
io.stderr("Run critical-gate init-policy --help for usage.");
|
|
317
|
-
return ExitCode.UsageError;
|
|
318
|
-
}
|
|
319
|
-
const root = io.readDiff().root;
|
|
320
|
-
const path = join(root, CRITICAL_GATE_CONFIG_FILE);
|
|
321
|
-
if (io.exists?.(path) === true && parsed.options.force !== true) {
|
|
322
|
-
io.stderr(`Refusing to overwrite existing ${CRITICAL_GATE_CONFIG_FILE}. Re-run with --force.`);
|
|
323
|
-
return ExitCode.UsageError;
|
|
324
|
-
}
|
|
325
|
-
io.writeFile(path, `${JSON.stringify(createDefaultPolicyConfig(io.now()), null, 2)}\n`);
|
|
326
|
-
io.stdout(`Wrote reviewable Critical Gate policy to ${path}.`);
|
|
327
|
-
return ExitCode.Pass;
|
|
328
|
-
}
|
|
329
|
-
function runInitAgentCommand(args, io) {
|
|
330
|
-
const parsed = parseInitAgentArgs(args);
|
|
331
|
-
if (!parsed.ok) {
|
|
332
|
-
io.stderr(parsed.error);
|
|
333
|
-
io.stderr("Run critical-gate init-agent --help for usage.");
|
|
334
|
-
return ExitCode.UsageError;
|
|
335
|
-
}
|
|
336
|
-
const root = io.readDiff().root;
|
|
337
|
-
const result = initAgentInstructions({
|
|
338
|
-
root,
|
|
339
|
-
cliCommand: parsed.options.cli,
|
|
340
|
-
io
|
|
341
|
-
});
|
|
342
|
-
if (result.updated) {
|
|
343
|
-
io.stdout(`${result.created ? "Created" : "Updated"} ${result.path} with Critical Gate agent instructions.`);
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
io.stdout(`${result.path} already contains the current Critical Gate agent instructions.`);
|
|
347
|
-
}
|
|
348
|
-
return ExitCode.Pass;
|
|
349
|
-
}
|
|
350
|
-
function readOptionalPackageJson(root, io) {
|
|
351
|
-
const path = join(root, "package.json");
|
|
352
|
-
if (io.exists?.(path) !== true) {
|
|
353
|
-
return undefined;
|
|
354
|
-
}
|
|
355
|
-
try {
|
|
356
|
-
return JSON.parse(io.readFile?.(path) ?? "");
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
return undefined;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function runAcceptCommand(args, io) {
|
|
363
|
-
const parsed = parseFlagArgs(args, ["--finding", "--reason"]);
|
|
364
|
-
if (!parsed.ok) {
|
|
365
|
-
io.stderr(parsed.error);
|
|
366
|
-
io.stderr("Run critical-gate accept --help for usage.");
|
|
367
|
-
return ExitCode.UsageError;
|
|
368
|
-
}
|
|
369
|
-
const finding = parsed.values["--finding"];
|
|
370
|
-
const reason = parsed.values["--reason"];
|
|
371
|
-
if (finding === undefined || reason === undefined) {
|
|
372
|
-
io.stderr("Missing required --finding or --reason value.");
|
|
373
|
-
io.stderr("Run critical-gate accept --help for usage.");
|
|
374
|
-
return ExitCode.UsageError;
|
|
375
|
-
}
|
|
376
|
-
const root = io.readDiff().root;
|
|
377
|
-
const updated = updateCriticalGateConfig(root, (config) => ({
|
|
378
|
-
...config,
|
|
379
|
-
learning: {
|
|
380
|
-
...config.learning,
|
|
381
|
-
acceptedFindings: upsertById(config.learning?.acceptedFindings ?? [], {
|
|
382
|
-
id: finding,
|
|
383
|
-
reason,
|
|
384
|
-
createdAt: io.now().toISOString()
|
|
385
|
-
})
|
|
386
|
-
}
|
|
387
|
-
}), {
|
|
388
|
-
exists: io.exists,
|
|
389
|
-
readFile: io.readFile,
|
|
390
|
-
writeFile: io.writeFile
|
|
391
|
-
});
|
|
392
|
-
io.stdout(`Accepted finding ${finding} in ${CRITICAL_GATE_CONFIG_FILE} (${updated.learning?.acceptedFindings?.length ?? 0} accepted finding rules).`);
|
|
393
|
-
return ExitCode.Pass;
|
|
394
|
-
}
|
|
395
|
-
function runTeachCommand(args, io) {
|
|
396
|
-
const parsed = parseFlagArgs(args, ["--id", "--when-changed", "--allow", "--reason"]);
|
|
397
|
-
if (!parsed.ok) {
|
|
398
|
-
io.stderr(parsed.error);
|
|
399
|
-
io.stderr("Run critical-gate teach --help for usage.");
|
|
400
|
-
return ExitCode.UsageError;
|
|
401
|
-
}
|
|
402
|
-
const id = parsed.values["--id"];
|
|
403
|
-
const whenChanged = parsed.values["--when-changed"];
|
|
404
|
-
const allow = parsed.values["--allow"]
|
|
405
|
-
?.split(",")
|
|
406
|
-
.map((entry) => entry.trim())
|
|
407
|
-
.filter(Boolean);
|
|
408
|
-
const reason = parsed.values["--reason"];
|
|
409
|
-
if (id === undefined ||
|
|
410
|
-
whenChanged === undefined ||
|
|
411
|
-
allow === undefined ||
|
|
412
|
-
reason === undefined) {
|
|
413
|
-
io.stderr("Missing required --id, --when-changed, --allow, or --reason value.");
|
|
414
|
-
io.stderr("Run critical-gate teach --help for usage.");
|
|
415
|
-
return ExitCode.UsageError;
|
|
416
|
-
}
|
|
417
|
-
const root = io.readDiff().root;
|
|
418
|
-
const updated = updateCriticalGateConfig(root, (config) => ({
|
|
419
|
-
...config,
|
|
420
|
-
learning: {
|
|
421
|
-
...config.learning,
|
|
422
|
-
expectedSupportFiles: upsertById(config.learning?.expectedSupportFiles ?? [], {
|
|
423
|
-
id,
|
|
424
|
-
whenChanged,
|
|
425
|
-
allow,
|
|
426
|
-
reason,
|
|
427
|
-
createdAt: io.now().toISOString()
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
}), {
|
|
431
|
-
exists: io.exists,
|
|
432
|
-
readFile: io.readFile,
|
|
433
|
-
writeFile: io.writeFile
|
|
434
|
-
});
|
|
435
|
-
io.stdout(`Taught expected support rule ${id} in ${CRITICAL_GATE_CONFIG_FILE} (${updated.learning?.expectedSupportFiles?.length ?? 0} support rules).`);
|
|
436
|
-
return ExitCode.Pass;
|
|
437
|
-
}
|
|
438
|
-
function getHelpText() {
|
|
439
|
-
return [
|
|
440
|
-
"critical-gate",
|
|
441
|
-
"",
|
|
442
|
-
"Usage:",
|
|
443
|
-
" critical-gate check --task <text> [--base <ref>] [--format json|markdown|sarif|repair|pr-comment] [--strict] [--output <path>]",
|
|
444
|
-
" critical-gate hook [--task <text>] [--base <ref>] [--output <path>]",
|
|
445
|
-
" critical-gate accept --finding <id> --reason <text>",
|
|
446
|
-
" critical-gate teach --id <id> --when-changed <glob> --allow <glob[,glob]> --reason <text>",
|
|
447
|
-
" critical-gate snapshot-api [--entrypoint <path>] [--output <path>]",
|
|
448
|
-
" critical-gate install-hooks [--hook pre-commit|pre-push|all] [--cli <command>] [--force]",
|
|
449
|
-
" critical-gate init-policy [--force]",
|
|
450
|
-
" critical-gate init-agent [--cli <command>]",
|
|
451
|
-
" critical-gate --version",
|
|
452
|
-
" critical-gate --help",
|
|
453
|
-
""
|
|
454
|
-
].join("\n");
|
|
455
|
-
}
|
|
456
|
-
function getCommandHelpText(command) {
|
|
457
|
-
if (command === "hook") {
|
|
458
|
-
return getHookHelpText();
|
|
459
|
-
}
|
|
460
|
-
if (command === "accept") {
|
|
461
|
-
return getAcceptHelpText();
|
|
462
|
-
}
|
|
463
|
-
if (command === "teach") {
|
|
464
|
-
return getTeachHelpText();
|
|
465
|
-
}
|
|
466
|
-
if (command === "snapshot-api") {
|
|
467
|
-
return getSnapshotApiHelpText();
|
|
468
|
-
}
|
|
469
|
-
if (command === "install-hooks") {
|
|
470
|
-
return getInstallHooksHelpText();
|
|
471
|
-
}
|
|
472
|
-
if (command === "init-policy") {
|
|
473
|
-
return getInitPolicyHelpText();
|
|
474
|
-
}
|
|
475
|
-
if (command === "init-agent") {
|
|
476
|
-
return getInitAgentHelpText();
|
|
477
|
-
}
|
|
478
|
-
return getCheckHelpText();
|
|
479
|
-
}
|
|
480
|
-
function getCheckHelpText() {
|
|
481
|
-
return [
|
|
482
|
-
"critical-gate check",
|
|
483
|
-
"",
|
|
484
|
-
"Required:",
|
|
485
|
-
" --task <text> Task intent, issue summary, or prompt",
|
|
486
|
-
"",
|
|
487
|
-
"Options:",
|
|
488
|
-
" --base <ref> Git baseline reference",
|
|
489
|
-
" --format <format> json, markdown, sarif, repair, or pr-comment",
|
|
490
|
-
" --fail-on <level> blocker, high, or medium; defaults to high",
|
|
491
|
-
" --staged Analyze staged changes with git diff --cached",
|
|
492
|
-
" --strict Fail on strict-mode findings once detectors exist",
|
|
493
|
-
" --output <path> Write report to a file instead of stdout",
|
|
494
|
-
""
|
|
495
|
-
].join("\n");
|
|
496
|
-
}
|
|
497
|
-
function getHookHelpText() {
|
|
498
|
-
return [
|
|
499
|
-
"critical-gate hook",
|
|
500
|
-
"",
|
|
501
|
-
"Options:",
|
|
502
|
-
" --task <text> Optional task intent; defaults to Codex completed feature implementation",
|
|
503
|
-
" --base <ref> Git baseline reference",
|
|
504
|
-
" --output <path> Write compact repair report to a file instead of stdout",
|
|
505
|
-
""
|
|
506
|
-
].join("\n");
|
|
507
|
-
}
|
|
508
|
-
function getAcceptHelpText() {
|
|
509
|
-
return [
|
|
510
|
-
"critical-gate accept",
|
|
511
|
-
"",
|
|
512
|
-
"Records an explicit accepted finding in .critical-gate.json.",
|
|
513
|
-
"",
|
|
514
|
-
"Required:",
|
|
515
|
-
" --finding <id> Exact finding id to accept",
|
|
516
|
-
" --reason <text> Reviewable reason for accepting the finding",
|
|
517
|
-
""
|
|
518
|
-
].join("\n");
|
|
519
|
-
}
|
|
520
|
-
function getTeachHelpText() {
|
|
521
|
-
return [
|
|
522
|
-
"critical-gate teach",
|
|
523
|
-
"",
|
|
524
|
-
"Records a repository-specific expected support-file rule in .critical-gate.json.",
|
|
525
|
-
"",
|
|
526
|
-
"Required:",
|
|
527
|
-
" --id <id> Stable rule id",
|
|
528
|
-
" --when-changed <glob> Changed file glob that activates the rule",
|
|
529
|
-
" --allow <glob[,glob]> Support-file glob or comma-separated globs to allow",
|
|
530
|
-
" --reason <text> Reviewable reason for the rule",
|
|
531
|
-
""
|
|
532
|
-
].join("\n");
|
|
533
|
-
}
|
|
534
|
-
function getSnapshotApiHelpText() {
|
|
535
|
-
return [
|
|
536
|
-
"critical-gate snapshot-api",
|
|
537
|
-
"",
|
|
538
|
-
"Generates a reviewable public API surface snapshot.",
|
|
539
|
-
"",
|
|
540
|
-
"Options:",
|
|
541
|
-
" --entrypoint <path> Public entrypoint to snapshot; may be passed more than once",
|
|
542
|
-
" --output <path> Snapshot path; defaults to .critical-gate/api-surface.json",
|
|
543
|
-
""
|
|
544
|
-
].join("\n");
|
|
545
|
-
}
|
|
546
|
-
function getInstallHooksHelpText() {
|
|
547
|
-
return [
|
|
548
|
-
"critical-gate install-hooks",
|
|
549
|
-
"",
|
|
550
|
-
"Installs reviewable local git hooks.",
|
|
551
|
-
"",
|
|
552
|
-
"Options:",
|
|
553
|
-
" --hook <hook> pre-commit, pre-push, or all; defaults to all",
|
|
554
|
-
" --cli <command> Critical Gate command/path used by the hook; defaults to critical-gate",
|
|
555
|
-
" --force Overwrite an existing generated or custom hook",
|
|
556
|
-
"",
|
|
557
|
-
"Defaults:",
|
|
558
|
-
" pre-commit runs staged changes with --fail-on blocker.",
|
|
559
|
-
" pre-push runs branch changes against ${CRITICAL_GATE_BASE:-origin/main} with --fail-on high.",
|
|
560
|
-
""
|
|
561
|
-
].join("\n");
|
|
562
|
-
}
|
|
563
|
-
function getInitPolicyHelpText() {
|
|
564
|
-
return [
|
|
565
|
-
"critical-gate init-policy",
|
|
566
|
-
"",
|
|
567
|
-
`Writes a starter ${CRITICAL_GATE_CONFIG_FILE} policy file.`,
|
|
568
|
-
"",
|
|
569
|
-
"Options:",
|
|
570
|
-
" --force Overwrite an existing policy file",
|
|
571
|
-
""
|
|
572
|
-
].join("\n");
|
|
573
|
-
}
|
|
574
|
-
function getInitAgentHelpText() {
|
|
575
|
-
return [
|
|
576
|
-
"critical-gate init-agent",
|
|
577
|
-
"",
|
|
578
|
-
"Creates or updates the managed Critical Gate section in AGENTS.md.",
|
|
579
|
-
"",
|
|
580
|
-
"Options:",
|
|
581
|
-
" --cli <command> Critical Gate command/path agents should run; defaults to critical-gate",
|
|
582
|
-
"",
|
|
583
|
-
"The command preserves existing AGENTS.md content and replaces only the managed Critical Gate block.",
|
|
584
|
-
""
|
|
585
|
-
].join("\n");
|
|
586
|
-
}
|
|
587
|
-
function isCommandName(value) {
|
|
588
|
-
return (value === "check" ||
|
|
589
|
-
value === "hook" ||
|
|
590
|
-
value === "accept" ||
|
|
591
|
-
value === "teach" ||
|
|
592
|
-
value === "snapshot-api" ||
|
|
593
|
-
value === "install-hooks" ||
|
|
594
|
-
value === "init-policy" ||
|
|
595
|
-
value === "init-agent");
|
|
596
|
-
}
|
|
597
|
-
function isReportFormat(value) {
|
|
598
|
-
return (value === "json" ||
|
|
599
|
-
value === "markdown" ||
|
|
600
|
-
value === "sarif" ||
|
|
601
|
-
value === "repair" ||
|
|
602
|
-
value === "pr-comment");
|
|
603
|
-
}
|
|
604
|
-
function isFailOnSeverity(value) {
|
|
605
|
-
return value === "blocker" || value === "high" || value === "medium";
|
|
606
|
-
}
|
|
607
|
-
function parseFlagArgs(args, allowedFlags) {
|
|
608
|
-
const values = {};
|
|
609
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
610
|
-
const arg = args[index];
|
|
611
|
-
if (!allowedFlags.includes(arg)) {
|
|
612
|
-
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
613
|
-
}
|
|
614
|
-
const value = args[index + 1];
|
|
615
|
-
if (value === undefined || value.startsWith("--")) {
|
|
616
|
-
return { ok: false, error: `Missing value for ${arg}.` };
|
|
617
|
-
}
|
|
618
|
-
values[arg] = value;
|
|
619
|
-
index += 1;
|
|
620
|
-
}
|
|
621
|
-
return { ok: true, values };
|
|
622
|
-
}
|
|
623
|
-
function parseSnapshotApiArgs(args) {
|
|
624
|
-
const options = {
|
|
625
|
-
entrypoints: []
|
|
626
|
-
};
|
|
627
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
628
|
-
const arg = args[index];
|
|
629
|
-
if (arg !== "--entrypoint" && arg !== "--output") {
|
|
630
|
-
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
631
|
-
}
|
|
632
|
-
const value = args[index + 1];
|
|
633
|
-
if (value === undefined || value.startsWith("--")) {
|
|
634
|
-
return { ok: false, error: `Missing value for ${arg}.` };
|
|
635
|
-
}
|
|
636
|
-
if (arg === "--entrypoint") {
|
|
637
|
-
options.entrypoints.push(value);
|
|
638
|
-
}
|
|
639
|
-
else {
|
|
640
|
-
options.output = value;
|
|
641
|
-
}
|
|
642
|
-
index += 1;
|
|
643
|
-
}
|
|
644
|
-
return {
|
|
645
|
-
ok: true,
|
|
646
|
-
options: {
|
|
647
|
-
output: options.output,
|
|
648
|
-
entrypoints: options.entrypoints.length > 0 ? options.entrypoints : undefined
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
function parseInstallHooksArgs(args) {
|
|
653
|
-
const options = {
|
|
654
|
-
hook: "all",
|
|
655
|
-
cli: "critical-gate",
|
|
656
|
-
force: false
|
|
657
|
-
};
|
|
658
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
659
|
-
const arg = args[index];
|
|
660
|
-
if (arg === "--force") {
|
|
661
|
-
options.force = true;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (arg !== "--hook" && arg !== "--cli") {
|
|
665
|
-
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
666
|
-
}
|
|
667
|
-
const value = args[index + 1];
|
|
668
|
-
if (value === undefined || value.startsWith("--")) {
|
|
669
|
-
return { ok: false, error: `Missing value for ${arg}.` };
|
|
670
|
-
}
|
|
671
|
-
if (arg === "--hook") {
|
|
672
|
-
if (value !== "pre-commit" && value !== "pre-push" && value !== "all") {
|
|
673
|
-
return { ok: false, error: "Invalid --hook value. Expected pre-commit, pre-push, or all." };
|
|
674
|
-
}
|
|
675
|
-
options.hook = value;
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
options.cli = value;
|
|
679
|
-
}
|
|
680
|
-
index += 1;
|
|
681
|
-
}
|
|
682
|
-
return { ok: true, options };
|
|
683
|
-
}
|
|
684
|
-
function parseInitPolicyArgs(args) {
|
|
685
|
-
const options = {
|
|
686
|
-
force: false
|
|
687
|
-
};
|
|
688
|
-
for (const arg of args) {
|
|
689
|
-
if (arg !== "--force") {
|
|
690
|
-
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
691
|
-
}
|
|
692
|
-
options.force = true;
|
|
693
|
-
}
|
|
694
|
-
return { ok: true, options };
|
|
695
|
-
}
|
|
696
|
-
function parseInitAgentArgs(args) {
|
|
697
|
-
const options = {
|
|
698
|
-
cli: "critical-gate"
|
|
699
|
-
};
|
|
700
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
701
|
-
const arg = args[index];
|
|
702
|
-
if (arg !== "--cli") {
|
|
703
|
-
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
704
|
-
}
|
|
705
|
-
const value = args[index + 1];
|
|
706
|
-
if (value === undefined || value.startsWith("--")) {
|
|
707
|
-
return { ok: false, error: `Missing value for ${arg}.` };
|
|
708
|
-
}
|
|
709
|
-
options.cli = value;
|
|
710
|
-
index += 1;
|
|
711
|
-
}
|
|
712
|
-
return { ok: true, options };
|
|
713
|
-
}
|
|
714
|
-
function renderGitHookScript(hook, cliCommand) {
|
|
715
|
-
const quotedCli = shellQuote(cliCommand);
|
|
716
|
-
if (hook === "pre-commit") {
|
|
717
|
-
return [
|
|
718
|
-
"#!/bin/sh",
|
|
719
|
-
"# Critical Gate pre-commit hook.",
|
|
720
|
-
"# Generated by `critical-gate install-hooks`; review before trusting.",
|
|
721
|
-
"set -eu",
|
|
722
|
-
'TASK="${CRITICAL_GATE_TASK:-Pre-commit staged change}"',
|
|
723
|
-
`${quotedCli} check --staged --task "$TASK" --format repair --fail-on blocker`,
|
|
724
|
-
""
|
|
725
|
-
].join("\n");
|
|
726
|
-
}
|
|
727
|
-
return [
|
|
728
|
-
"#!/bin/sh",
|
|
729
|
-
"# Critical Gate pre-push hook.",
|
|
730
|
-
"# Generated by `critical-gate install-hooks`; review before trusting.",
|
|
731
|
-
"set -eu",
|
|
732
|
-
'TASK="${CRITICAL_GATE_TASK:-Pre-push branch change}"',
|
|
733
|
-
'BASE="${CRITICAL_GATE_BASE:-origin/main}"',
|
|
734
|
-
`${quotedCli} check --base "$BASE" --task "$TASK" --format repair --fail-on high`,
|
|
735
|
-
""
|
|
736
|
-
].join("\n");
|
|
737
|
-
}
|
|
738
|
-
function shellQuote(value) {
|
|
739
|
-
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
|
|
740
|
-
return value;
|
|
741
|
-
}
|
|
742
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
743
|
-
}
|
|
744
|
-
function upsertById(entries, next) {
|
|
745
|
-
return [...entries.filter((entry) => entry.id !== next.id), next];
|
|
746
|
-
}
|
|
747
|
-
export function isCliEntrypoint(importMetaUrl, argvPath = process.argv[1]) {
|
|
748
|
-
return (argvPath !== undefined &&
|
|
749
|
-
normalizeExecutablePath(fileURLToPath(importMetaUrl)) === normalizeExecutablePath(argvPath));
|
|
750
|
-
}
|
|
751
|
-
function normalizeExecutablePath(path) {
|
|
752
|
-
let normalized = resolve(path);
|
|
753
|
-
try {
|
|
754
|
-
normalized = realpathSync.native(normalized);
|
|
755
|
-
}
|
|
756
|
-
catch {
|
|
757
|
-
// Fall back to the resolved path when the target is virtual or has already been removed.
|
|
758
|
-
}
|
|
759
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
760
|
-
}
|
|
2
|
+
import { isCliEntrypoint } from "./cli/entrypoint.js";
|
|
3
|
+
import { main } from "./cli/main.js";
|
|
4
|
+
import { ExitCode as ExitCodeValue } from "./cli/types.js";
|
|
5
|
+
export { isCliEntrypoint } from "./cli/entrypoint.js";
|
|
6
|
+
export { CLI_VERSION, main } from "./cli/main.js";
|
|
7
|
+
export const ExitCode = ExitCodeValue;
|
|
761
8
|
if (isCliEntrypoint(import.meta.url)) {
|
|
762
9
|
process.exitCode = main();
|
|
763
10
|
}
|