pr-checkmate 3.3.0 → 4.0.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 (113) hide show
  1. package/LICENSE +8 -3
  2. package/README.md +53 -4
  3. package/dist/cjs/agent/hook.d.ts +37 -0
  4. package/dist/cjs/agent/hook.js +175 -0
  5. package/dist/cjs/agent/registration.d.ts +10 -0
  6. package/dist/cjs/agent/registration.js +28 -0
  7. package/dist/cjs/agent/setup.d.ts +20 -0
  8. package/dist/cjs/agent/setup.js +229 -0
  9. package/dist/cjs/cli.js +88 -0
  10. package/dist/cjs/config/constants.d.ts +2 -0
  11. package/dist/cjs/config/constants.js +5 -1
  12. package/dist/cjs/config/pr-checkmate.template.json +10 -0
  13. package/dist/cjs/core/checks/findings.js +33 -7
  14. package/dist/cjs/core/checks/git/large-files.d.ts +1 -0
  15. package/dist/cjs/core/checks/git/large-files.js +3 -3
  16. package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
  17. package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
  18. package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
  19. package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
  20. package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
  21. package/dist/cjs/core/checks/security/diff-security.js +21 -1
  22. package/dist/cjs/core/checks/security/security.d.ts +0 -13
  23. package/dist/cjs/core/checks/security/security.js +27 -0
  24. package/dist/cjs/core/checks/security/workflow-security.js +33 -0
  25. package/dist/cjs/core/commands.d.ts +6 -0
  26. package/dist/cjs/core/commands.js +11 -1
  27. package/dist/cjs/core/finding-store.d.ts +16 -0
  28. package/dist/cjs/core/finding-store.js +73 -0
  29. package/dist/cjs/core/registry.js +2 -0
  30. package/dist/cjs/core/reporters.js +9 -0
  31. package/dist/cjs/core/run-checks.js +15 -5
  32. package/dist/cjs/core/sarif.d.ts +13 -0
  33. package/dist/cjs/core/sarif.js +60 -0
  34. package/dist/cjs/core/types.d.ts +7 -2
  35. package/dist/cjs/index.d.ts +2 -1
  36. package/dist/cjs/index.js +4 -1
  37. package/dist/cjs/init.js +11 -0
  38. package/dist/cjs/interfaces/index.d.ts +12 -0
  39. package/dist/cjs/mcp/config-tools.d.ts +30 -0
  40. package/dist/cjs/mcp/config-tools.js +320 -0
  41. package/dist/cjs/mcp/dependency.d.ts +33 -0
  42. package/dist/cjs/mcp/dependency.js +407 -0
  43. package/dist/cjs/mcp/protocol.d.ts +97 -0
  44. package/dist/cjs/mcp/protocol.js +181 -0
  45. package/dist/cjs/mcp/review.d.ts +63 -0
  46. package/dist/cjs/mcp/review.js +150 -0
  47. package/dist/cjs/mcp/serve.d.ts +8 -0
  48. package/dist/cjs/mcp/serve.js +49 -0
  49. package/dist/cjs/mcp/settings.d.ts +13 -0
  50. package/dist/cjs/mcp/settings.js +50 -0
  51. package/dist/cjs/mcp/tools.d.ts +23 -0
  52. package/dist/cjs/mcp/tools.js +633 -0
  53. package/dist/cjs/utils/execa.d.ts +7 -0
  54. package/dist/cjs/utils/execa.js +41 -1
  55. package/dist/cjs/utils/result-collector.d.ts +17 -1
  56. package/dist/cjs/utils/result-collector.js +7 -2
  57. package/dist/esm/agent/hook.d.ts +37 -0
  58. package/dist/esm/agent/hook.js +167 -0
  59. package/dist/esm/agent/registration.d.ts +10 -0
  60. package/dist/esm/agent/registration.js +21 -0
  61. package/dist/esm/agent/setup.d.ts +20 -0
  62. package/dist/esm/agent/setup.js +220 -0
  63. package/dist/esm/cli.js +86 -1
  64. package/dist/esm/config/constants.d.ts +2 -0
  65. package/dist/esm/config/constants.js +4 -0
  66. package/dist/esm/config/pr-checkmate.template.json +10 -0
  67. package/dist/esm/core/checks/findings.js +33 -7
  68. package/dist/esm/core/checks/git/large-files.d.ts +1 -0
  69. package/dist/esm/core/checks/git/large-files.js +1 -1
  70. package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
  71. package/dist/esm/core/checks/pr/pr-size-check.js +2 -2
  72. package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
  73. package/dist/esm/core/checks/security/agent-instructions.js +146 -0
  74. package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
  75. package/dist/esm/core/checks/security/diff-security.js +20 -0
  76. package/dist/esm/core/checks/security/security.d.ts +0 -13
  77. package/dist/esm/core/checks/security/security.js +27 -0
  78. package/dist/esm/core/checks/security/workflow-security.js +33 -0
  79. package/dist/esm/core/commands.d.ts +6 -0
  80. package/dist/esm/core/commands.js +10 -0
  81. package/dist/esm/core/finding-store.d.ts +16 -0
  82. package/dist/esm/core/finding-store.js +67 -0
  83. package/dist/esm/core/registry.js +2 -0
  84. package/dist/esm/core/reporters.js +9 -0
  85. package/dist/esm/core/run-checks.js +15 -5
  86. package/dist/esm/core/sarif.d.ts +13 -0
  87. package/dist/esm/core/sarif.js +57 -0
  88. package/dist/esm/core/types.d.ts +7 -2
  89. package/dist/esm/index.d.ts +2 -1
  90. package/dist/esm/index.js +2 -0
  91. package/dist/esm/init.js +12 -1
  92. package/dist/esm/interfaces/index.d.ts +12 -0
  93. package/dist/esm/mcp/config-tools.d.ts +30 -0
  94. package/dist/esm/mcp/config-tools.js +310 -0
  95. package/dist/esm/mcp/dependency.d.ts +33 -0
  96. package/dist/esm/mcp/dependency.js +402 -0
  97. package/dist/esm/mcp/protocol.d.ts +97 -0
  98. package/dist/esm/mcp/protocol.js +175 -0
  99. package/dist/esm/mcp/review.d.ts +63 -0
  100. package/dist/esm/mcp/review.js +139 -0
  101. package/dist/esm/mcp/serve.d.ts +8 -0
  102. package/dist/esm/mcp/serve.js +43 -0
  103. package/dist/esm/mcp/settings.d.ts +13 -0
  104. package/dist/esm/mcp/settings.js +43 -0
  105. package/dist/esm/mcp/tools.d.ts +23 -0
  106. package/dist/esm/mcp/tools.js +626 -0
  107. package/dist/esm/utils/execa.d.ts +7 -0
  108. package/dist/esm/utils/execa.js +40 -1
  109. package/dist/esm/utils/result-collector.d.ts +17 -1
  110. package/dist/esm/utils/result-collector.js +7 -2
  111. package/package.json +6 -2
  112. package/pr-checkmate.schema.json +35 -0
  113. package/src/config/pr-checkmate.schema.json +35 -0
@@ -55,6 +55,39 @@ exports.WORKFLOW_PATTERNS = [
55
55
  label: 'pull_request_target trigger (runs with repo secrets on untrusted PRs)',
56
56
  severity: 'warn',
57
57
  },
58
+ {
59
+ // The second half of the "pwn request": `pull_request_target` alone is harmless,
60
+ // checking out the PR's own code under it hands the attacker a privileged
61
+ // runner. One line cannot see the trigger, so this stays a warning.
62
+ pattern: /^\+(?!\+)\s*(?:ref|repository):\s*['"]?\$\{\{\s*github\.event\.pull_request\.head\.(?:sha|ref|repo\.full_name)\s*\}\}/,
63
+ label: 'checkout of the PR head (code execution with secrets under pull_request_target)',
64
+ severity: 'warn',
65
+ },
66
+ {
67
+ pattern: /^\+(?!\+)\s*permissions:\s*write-all\b/,
68
+ label: 'permissions: write-all (grant the token only the scopes the job needs)',
69
+ severity: 'error',
70
+ },
71
+ {
72
+ pattern: /^\+(?!\+)\s*secrets:\s*inherit\b/,
73
+ label: 'secrets: inherit (hands every repository secret to the called workflow)',
74
+ severity: 'warn',
75
+ },
76
+ {
77
+ pattern: /^\+(?!\+)\s*ACTIONS_ALLOW_UNSECURE_COMMANDS:\s*['"]?true\b/i,
78
+ label: 'ACTIONS_ALLOW_UNSECURE_COMMANDS (re-enables the deprecated set-env / add-path injection vector)',
79
+ severity: 'error',
80
+ },
81
+ {
82
+ pattern: /^\+(?!\+).*\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:sh|bash)\b/i,
83
+ label: 'piping a download straight to a shell (curl/wget … | sh)',
84
+ severity: 'warn',
85
+ },
86
+ {
87
+ pattern: /^\+(?!\+)\s*runs-on:\s*\[?\s*['"]?self-hosted\b/,
88
+ label: 'self-hosted runner (a PR from a fork can run code on your own infrastructure)',
89
+ severity: 'warn',
90
+ },
58
91
  ];
59
92
  exports.workflowSecurityCheck = {
60
93
  name: 'Workflow Security',
@@ -22,6 +22,12 @@ export declare const COMMAND_TO_CHECK: Record<string, string>;
22
22
  * and not a reading of our source.
23
23
  */
24
24
  export declare const TOOLING_COMMANDS: readonly string[];
25
+ /**
26
+ * Commands that serve AI agents rather than review a diff. They share the CLI's
27
+ * entry point and nothing else: no scope flags apply, and `mcp` in particular owns
28
+ * stdout for the protocol, so none of them may be confused with a check name.
29
+ */
30
+ export declare const AGENT_COMMANDS: readonly string[];
25
31
  export declare const STANDALONE_COMMANDS: readonly string[];
26
32
  /** Flags accepted anywhere on the command line, with what each one means. */
27
33
  export declare const FLAGS: Record<string, string>;
@@ -12,7 +12,7 @@
12
12
  * reports success having verified nothing at all.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.FLAGS = exports.STANDALONE_COMMANDS = exports.TOOLING_COMMANDS = exports.COMMAND_TO_CHECK = void 0;
15
+ exports.FLAGS = exports.STANDALONE_COMMANDS = exports.AGENT_COMMANDS = exports.TOOLING_COMMANDS = exports.COMMAND_TO_CHECK = void 0;
16
16
  /** Maps short CLI command names to their registry check names. */
17
17
  exports.COMMAND_TO_CHECK = {
18
18
  // Every check that can fail a run is reachable on its own, so "why does this
@@ -44,6 +44,7 @@ exports.COMMAND_TO_CHECK = {
44
44
  'license-header': 'License Header',
45
45
  'config-validation': 'Config Validation',
46
46
  'dockerfile-security': 'Dockerfile Security',
47
+ 'agent-instructions': 'Agent Instructions',
47
48
  'case-collision': 'Case Collision',
48
49
  'vuln-scan': 'Vuln Scan',
49
50
  'leftover-debug': 'Leftover Debug',
@@ -79,14 +80,23 @@ exports.COMMAND_TO_CHECK = {
79
80
  * and not a reading of our source.
80
81
  */
81
82
  exports.TOOLING_COMMANDS = ['install', 'doctor', 'cache-prune'];
83
+ /**
84
+ * Commands that serve AI agents rather than review a diff. They share the CLI's
85
+ * entry point and nothing else: no scope flags apply, and `mcp` in particular owns
86
+ * stdout for the protocol, so none of them may be confused with a check name.
87
+ */
88
+ exports.AGENT_COMMANDS = ['mcp', 'hook', 'agent-setup'];
82
89
  exports.STANDALONE_COMMANDS = [
83
90
  'init',
84
91
  'all',
85
92
  'precommit',
86
93
  ...exports.TOOLING_COMMANDS,
94
+ ...exports.AGENT_COMMANDS,
87
95
  ];
88
96
  /** Flags accepted anywhere on the command line, with what each one means. */
89
97
  exports.FLAGS = {
90
98
  '--full': 'review every tracked file instead of the diff (the default outside CI)',
91
99
  '--all': 'with "install": fetch every profile, not only the ones this repository needs',
100
+ '--sarif': 'write findings as SARIF 2.1.0 to pr-checkmate.sarif (check-running commands only)',
101
+ '--no-hook': 'with "agent-setup": skip the git-commit gate for Claude Code',
92
102
  };
@@ -0,0 +1,16 @@
1
+ import type { Finding } from '../utils/result-collector.js';
2
+ /** Run `work` collecting every finding filed inside it. */
3
+ export declare function collectFindings<T>(work: () => Promise<T>): Promise<{
4
+ result: T;
5
+ findings: Finding[];
6
+ }>;
7
+ /** A scanner saw `sample` under `label` at `file:line`. Claimed later by {@link fileFindings}. */
8
+ export declare function noteLocation(label: string, sample: string, file: string, line: number): void;
9
+ /**
10
+ * Turn the findings that survived filtering into located ones. Each sample claims
11
+ * the oldest location noted for it, so the same line appearing twice resolves to
12
+ * two places rather than one place twice.
13
+ */
14
+ export declare function fileFindings(bucket: Record<string, string[]>, severity: Finding['severity']): void;
15
+ /** File a finding directly, for scanners that already know exactly where it is. */
16
+ export declare function addFinding(finding: Finding): void;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectFindings = collectFindings;
4
+ exports.noteLocation = noteLocation;
5
+ exports.fileFindings = fileFindings;
6
+ exports.addFinding = addFinding;
7
+ /**
8
+ * Where a finding was, carried from the scanner that saw it to the report that
9
+ * names it.
10
+ *
11
+ * Scanners reduce what they find to strings in a bucket — `"debugger statement" →
12
+ * ["debugger;"]` — because the report only ever needed a count and a sample. An
13
+ * agent needs more: which file, which line. Rewriting forty checks to carry that
14
+ * would have been forty chances to disagree with each other, so the location is
15
+ * noted once, where the diff is walked, and attached once, where the buckets are
16
+ * filed.
17
+ *
18
+ * It is attached at filing time, not scan time, on purpose. `ignore` and
19
+ * `severityOverrides` run in between, and a finding the client has silenced must
20
+ * not reach an agent that would then dutifully "fix" it.
21
+ *
22
+ * The store is an `AsyncLocalStorage` scope per check. Checks of one phase run in
23
+ * parallel, so a module-level list would mix their findings; a scope cannot. Code
24
+ * running outside `collectFindings` (the library API, unit tests) finds no store
25
+ * and every call here does nothing.
26
+ */
27
+ const node_async_hooks_1 = require("node:async_hooks");
28
+ /** More than this and the list is noise to a reader and a cost to an agent. */
29
+ const MAX_FINDINGS_PER_CHECK = 200;
30
+ const scope = new node_async_hooks_1.AsyncLocalStorage();
31
+ const keyOf = (label, sample) => `${label}\u0000${sample}`;
32
+ /** Run `work` collecting every finding filed inside it. */
33
+ async function collectFindings(work) {
34
+ const store = { found: [], pending: new Map() };
35
+ const result = await scope.run(store, work);
36
+ return { result, findings: store.found };
37
+ }
38
+ /** A scanner saw `sample` under `label` at `file:line`. Claimed later by {@link fileFindings}. */
39
+ function noteLocation(label, sample, file, line) {
40
+ const store = scope.getStore();
41
+ if (!store)
42
+ return;
43
+ const key = keyOf(label, sample);
44
+ const queue = store.pending.get(key) ?? [];
45
+ queue.push({ file, line });
46
+ store.pending.set(key, queue);
47
+ }
48
+ /**
49
+ * Turn the findings that survived filtering into located ones. Each sample claims
50
+ * the oldest location noted for it, so the same line appearing twice resolves to
51
+ * two places rather than one place twice.
52
+ */
53
+ function fileFindings(bucket, severity) {
54
+ const store = scope.getStore();
55
+ if (!store)
56
+ return;
57
+ for (const [label, samples] of Object.entries(bucket)) {
58
+ for (const sample of samples) {
59
+ if (store.found.length >= MAX_FINDINGS_PER_CHECK)
60
+ return;
61
+ const where = store.pending.get(keyOf(label, sample))?.shift();
62
+ if (!where)
63
+ continue;
64
+ store.found.push({ ...where, rule: label, snippet: sample, severity });
65
+ }
66
+ }
67
+ }
68
+ /** File a finding directly, for scanners that already know exactly where it is. */
69
+ function addFinding(finding) {
70
+ const store = scope.getStore();
71
+ if (store && store.found.length < MAX_FINDINGS_PER_CHECK)
72
+ store.found.push(finding);
73
+ }
@@ -41,6 +41,7 @@ const package_size_js_1 = require("./checks/quality/package-size.js");
41
41
  const spellcheck_js_1 = require("./checks/quality/spellcheck.js");
42
42
  const symlinks_js_1 = require("./checks/quality/symlinks.js");
43
43
  const yaml_lint_js_1 = require("./checks/quality/yaml-lint.js");
44
+ const agent_instructions_js_1 = require("./checks/security/agent-instructions.js");
44
45
  const diff_security_js_1 = require("./checks/security/diff-security.js");
45
46
  const dockerfile_security_js_1 = require("./checks/security/dockerfile-security.js");
46
47
  const migration_safety_js_1 = require("./checks/security/migration-safety.js");
@@ -66,6 +67,7 @@ exports.REGISTRY = [
66
67
  diff_security_js_1.diffSecurityCheck,
67
68
  workflow_security_js_1.workflowSecurityCheck,
68
69
  dockerfile_security_js_1.dockerfileSecurityCheck,
70
+ agent_instructions_js_1.agentInstructionsCheck,
69
71
  case_collision_js_1.caseCollisionCheck,
70
72
  vuln_scan_js_1.vulnScanCheck,
71
73
  grype_scan_js_1.grypeScanCheck,
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildMarkdownReport = buildMarkdownReport;
7
7
  exports.report = report;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
+ const registration_js_1 = require("../agent/registration.js");
10
+ const constants_js_1 = require("../config/constants.js");
11
+ const settings_js_1 = require("../mcp/settings.js");
9
12
  const http_js_1 = require("../utils/http.js");
10
13
  const index_js_1 = require("../utils/index.js");
11
14
  const package_version_js_1 = require("../utils/package-version.js");
@@ -107,6 +110,12 @@ function printStdout(report, cfg) {
107
110
  if (cfg.include?.docsLink !== false) {
108
111
  // The docs cover every check and config key.
109
112
  index_js_1.logger.print(' DOCUMENTATION: https://pr-checkmate-docs.pages.dev/');
113
+ // Said once per project, and never in CI, where nobody is there to act on it.
114
+ if (!process.env.CI &&
115
+ (0, settings_js_1.readMcpSettings)(process.cwd()).enabled &&
116
+ !(0, registration_js_1.isMcpRegistered)(process.cwd())) {
117
+ index_js_1.logger.print(` AI AGENT: ${constants_js_1.AGENT_SETUP_COMMAND} — your agent then writes code that passes.`);
118
+ }
110
119
  }
111
120
  }
112
121
  /** Network settings shared by every platform call. */
@@ -9,6 +9,7 @@ const pool_js_1 = require("../utils/pool.js");
9
9
  const resolve_bin_js_1 = require("../utils/resolve-bin.js");
10
10
  const result_collector_js_1 = require("../utils/result-collector.js");
11
11
  const context_js_1 = require("./context.js");
12
+ const finding_store_js_1 = require("./finding-store.js");
12
13
  const prepare_js_1 = require("./install/prepare.js");
13
14
  const registry_js_1 = require("./registry.js");
14
15
  const reporters_js_1 = require("./reporters.js");
@@ -174,15 +175,24 @@ async function runOne(check, ctx, collector, heavy, tooling) {
174
175
  }
175
176
  const started = Date.now();
176
177
  const timeoutMs = ctx.config.run?.checkTimeoutMs ?? 300000;
177
- const execute = () => (0, pool_js_1.withTimeout)(check.run(ctx), timeoutMs, () => {
178
- index_js_1.logger.warn(`[runChecks]: ⏱️ ${check.name} exceeded ${timeoutMs}ms`);
179
- return { status: 'warn', details: `timed out after ${timeoutMs}ms` };
180
- });
178
+ // Locations noted while this check runs, collected per check: the phase runs
179
+ // its checks in parallel, so a shared list would mix their findings.
180
+ let located = [];
181
+ const execute = async () => {
182
+ const { result, findings } = await (0, finding_store_js_1.collectFindings)(() => (0, pool_js_1.withTimeout)(check.run(ctx), timeoutMs, () => {
183
+ index_js_1.logger.warn(`[runChecks]: ⏱️ ${check.name} exceeded ${timeoutMs}ms`);
184
+ return { status: 'warn', details: `timed out after ${timeoutMs}ms` };
185
+ }));
186
+ located = findings;
187
+ return result;
188
+ };
181
189
  const outcome = check.weight === 'heavy' ? await heavy.run(execute) : await execute();
182
190
  if (ctx.config.run?.timings) {
183
191
  index_js_1.logger.info(`[runChecks]: ${check.name} took ${Date.now() - started}ms`);
184
192
  }
185
- collector.register(check.name, applySeverity(check.name, outcome.status, ctx), outcome.details);
193
+ collector.register(check.name, applySeverity(check.name, outcome.status, ctx), outcome.details,
194
+ // A clean result has nothing to locate, even when `failOn: never` filed some.
195
+ outcome.status === 'pass' ? undefined : (outcome.findings ?? located));
186
196
  }
187
197
  catch (err) {
188
198
  // An unexpected throw in a blocking check fails the run; in informational/format
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A report as SARIF 2.1.0 — the format GitHub code scanning, VS Code and most
3
+ * security dashboards read, so a finding shows up inline on the line it names
4
+ * instead of only in a summary table.
5
+ *
6
+ * Only what a consumer can use is written. A failed or warned check that located
7
+ * its problems yields one result per location; one that could not yields a single
8
+ * result with no location, because a finding with nowhere to point is still a
9
+ * finding. Passes and skips are not results.
10
+ */
11
+ import type { CheckReport } from './types.js';
12
+ /** Convert a report to a SARIF log object, ready for `JSON.stringify`. */
13
+ export declare function toSarif(report: CheckReport, toolVersion: string): Record<string, unknown>;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toSarif = toSarif;
4
+ const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
5
+ const ruleIdFor = (check, rule) => (rule ? `${check}/${rule}` : check);
6
+ function locatedResult(check, f) {
7
+ return {
8
+ ruleId: ruleIdFor(check.name, f.rule),
9
+ level: f.severity === 'error' ? 'error' : 'warning',
10
+ message: { text: f.snippet ? `${f.rule}: ${f.snippet}` : f.rule },
11
+ locations: [
12
+ {
13
+ physicalLocation: {
14
+ artifactLocation: { uri: f.file },
15
+ ...(f.line !== undefined && { region: { startLine: f.line } }),
16
+ },
17
+ },
18
+ ],
19
+ };
20
+ }
21
+ function summaryResult(check) {
22
+ return {
23
+ ruleId: ruleIdFor(check.name),
24
+ level: check.status === 'fail' ? 'error' : 'warning',
25
+ message: { text: check.details ?? `${check.name} reported a problem` },
26
+ };
27
+ }
28
+ /** Convert a report to a SARIF log object, ready for `JSON.stringify`. */
29
+ function toSarif(report, toolVersion) {
30
+ const results = [];
31
+ const rules = new Map();
32
+ for (const check of report.results) {
33
+ if (check.status !== 'fail' && check.status !== 'warn')
34
+ continue;
35
+ const found = check.findings && check.findings.length > 0
36
+ ? check.findings.map(f => locatedResult(check, f))
37
+ : [summaryResult(check)];
38
+ for (const r of found) {
39
+ results.push(r);
40
+ rules.set(r.ruleId, { id: r.ruleId, name: r.ruleId });
41
+ }
42
+ }
43
+ return {
44
+ $schema: SARIF_SCHEMA,
45
+ version: '2.1.0',
46
+ runs: [
47
+ {
48
+ tool: {
49
+ driver: {
50
+ name: 'pr-checkmate',
51
+ version: toolVersion,
52
+ informationUri: 'https://www.npmjs.com/package/pr-checkmate',
53
+ rules: [...rules.values()],
54
+ },
55
+ },
56
+ results,
57
+ },
58
+ ],
59
+ };
60
+ }
@@ -1,7 +1,7 @@
1
1
  import type { PRCheckMateConfig, ReporterConfig } from '../interfaces/index.js';
2
2
  import type { Language, LanguageUsage } from '../utils/detect-language.js';
3
- import type { CheckResult, CheckStatus } from '../utils/result-collector.js';
4
- export type { CheckResult, CheckStatus, Language, LanguageUsage };
3
+ import type { CheckResult, CheckStatus, Finding } from '../utils/result-collector.js';
4
+ export type { CheckResult, CheckStatus, Finding, Language, LanguageUsage };
5
5
  /**
6
6
  * Execution phases, run strictly in order. Within a phase checks run in parallel.
7
7
  * `format` runs last because formatters (prettier) may write files.
@@ -67,6 +67,11 @@ export interface CheckContext {
67
67
  export interface CheckOutcome {
68
68
  status: CheckStatus;
69
69
  details?: string;
70
+ /**
71
+ * Where the problems are. Left to the orchestrator for pattern-based checks,
72
+ * which note locations as they scan; a check that knows better may set it.
73
+ */
74
+ findings?: Finding[];
70
75
  }
71
76
  /**
72
77
  * A single quality gate. `applies` decides relevance (language/marker gating);
@@ -4,8 +4,9 @@
4
4
  * @module pr-checkmate
5
5
  */
6
6
  export { runChecks, buildContext, REGISTRY } from './core/index.js';
7
+ export { toSarif } from './core/sarif.js';
7
8
  export { pass, warn, fail, skip, defineCheck } from './core/index.js';
8
- export type { Check, CheckContext, CheckOutcome, CheckReport, CheckResult, CheckStatus, Language, Phase, ReporterFn, ReporterKind, RunOptions, } from './core/index.js';
9
+ export type { Check, CheckContext, CheckOutcome, CheckReport, CheckResult, CheckStatus, Finding, Language, Phase, ReporterFn, ReporterKind, RunOptions, } from './core/index.js';
9
10
  export { getSourcePath, getSourcePaths, getIgnoredDirs, getPackageConfigPath, hasLocalConfig, } from './config/index.js';
10
11
  export type { PRCheckMateConfig } from './interfaces/index.js';
11
12
  export { Logger } from './utils/logger.js';
package/dist/cjs/index.js CHANGED
@@ -5,13 +5,16 @@
5
5
  * @module pr-checkmate
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.Logger = exports.hasLocalConfig = exports.getPackageConfigPath = exports.getIgnoredDirs = exports.getSourcePaths = exports.getSourcePath = exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.REGISTRY = exports.buildContext = exports.runChecks = void 0;
8
+ exports.Logger = exports.hasLocalConfig = exports.getPackageConfigPath = exports.getIgnoredDirs = exports.getSourcePaths = exports.getSourcePath = exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.toSarif = exports.REGISTRY = exports.buildContext = exports.runChecks = void 0;
9
9
  // Primary programmatic API: run checks and receive a structured report.
10
10
  // Never throws for violations, never calls process.exit — the caller decides.
11
11
  var index_js_1 = require("./core/index.js");
12
12
  Object.defineProperty(exports, "runChecks", { enumerable: true, get: function () { return index_js_1.runChecks; } });
13
13
  Object.defineProperty(exports, "buildContext", { enumerable: true, get: function () { return index_js_1.buildContext; } });
14
14
  Object.defineProperty(exports, "REGISTRY", { enumerable: true, get: function () { return index_js_1.REGISTRY; } });
15
+ // SARIF 2.1.0 export of a report, for code-scanning dashboards and editors.
16
+ var sarif_js_1 = require("./core/sarif.js");
17
+ Object.defineProperty(exports, "toSarif", { enumerable: true, get: function () { return sarif_js_1.toSarif; } });
15
18
  // Authoring helpers for consumers writing their own checks (plugins).
16
19
  var index_js_2 = require("./core/index.js");
17
20
  Object.defineProperty(exports, "pass", { enumerable: true, get: function () { return index_js_2.pass; } });
package/dist/cjs/init.js CHANGED
@@ -419,6 +419,16 @@ async function createConfig(root) {
419
419
  * update notice insisted `init` must not be skipped after an upgrade. Defaults
420
420
  * now apply when the config is loaded, so there is nothing to top up.
421
421
  */
422
+ /** Adds the agent-setup command to a client's `commands` block when it is missing; nothing else in it is touched. */
423
+ function withAgentCommand(commands) {
424
+ if (commands === undefined)
425
+ return undefined;
426
+ if (typeof commands !== 'object' || commands === null || Array.isArray(commands))
427
+ return commands;
428
+ if (index_js_1.AGENT_SETUP_COMMAND in commands)
429
+ return commands;
430
+ return { ...commands, [index_js_1.AGENT_SETUP_COMMAND]: index_js_1.DEFAULT_COMMANDS[index_js_1.AGENT_SETUP_COMMAND] };
431
+ }
422
432
  async function updateConfig(root) {
423
433
  index_js_2.logger.info('[init]: pr-checkmate.json already exists — updating it.');
424
434
  try {
@@ -449,6 +459,7 @@ async function updateConfig(root) {
449
459
  // when missing; a client's own block is never touched.
450
460
  grypeScan: json.grypeScan ?? structuredClone(GRYPE_SCAN_SEED),
451
461
  ...templateExtras({ ...json, grypeScan: true }),
462
+ commands: withAgentCommand(json.commands),
452
463
  languages,
453
464
  };
454
465
  fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
@@ -317,6 +317,18 @@ export interface PRCheckMateConfig {
317
317
  severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
318
318
  ignorePaths?: string[];
319
319
  };
320
+ mcp?: {
321
+ enabled?: boolean;
322
+ disabledTools?: string[];
323
+ readOnly?: boolean;
324
+ };
325
+ agentInstructions?: {
326
+ enabled?: boolean;
327
+ ignore?: string[];
328
+ failOn?: 'error' | 'warn' | 'never';
329
+ severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
330
+ ignorePaths?: string[];
331
+ };
320
332
  dockerfileSecurity?: {
321
333
  enabled?: boolean;
322
334
  ignore?: string[];
@@ -0,0 +1,30 @@
1
+ import type { ToolResult } from './protocol.js';
2
+ type Json = Record<string, unknown>;
3
+ /** What a key means, from the schema that ships with the package. */
4
+ export declare function explainConfig(key?: string): ToolResult;
5
+ /** Problems in a proposed config, phrased so the agent can correct them. Empty means it is acceptable. */
6
+ export declare function validateConfig(config: Json, cwd: string): string[];
7
+ /** Validate and write `config` into the repository's `pr-checkmate.json`. */
8
+ export declare function writeConfig(cwd: string, config: unknown, overwrite: boolean): ToolResult;
9
+ interface Suggestion {
10
+ proposed: Json;
11
+ notes: string[];
12
+ }
13
+ /**
14
+ * Look at the repository and propose a config.
15
+ *
16
+ * The baseline is a whole-repository run of the fast checks. A check that already
17
+ * fails is proposed as `warn`: the gate is on from the first commit, the existing
18
+ * debt is visible, and nothing new can slip in unnoticed — then each check is
19
+ * promoted back to blocking as it reaches zero. Proposing `error` everywhere would
20
+ * make the first CI run red for reasons nobody on the pull request caused, and the
21
+ * usual response to that is to switch the whole tool off.
22
+ */
23
+ export declare function suggestConfig(cwd: string): Promise<Suggestion & {
24
+ languages: string[];
25
+ }>;
26
+ /** `suggest_config` as the text an agent reads. */
27
+ export declare function formatSuggestion(s: Suggestion & {
28
+ languages: string[];
29
+ }): ToolResult;
30
+ export {};