pr-checkmate 3.2.3 → 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.
- package/.eslintrc.js +3 -9
- package/LICENSE +8 -3
- package/README.md +54 -5
- package/dist/cjs/agent/hook.d.ts +37 -0
- package/dist/cjs/agent/hook.js +175 -0
- package/dist/cjs/agent/registration.d.ts +10 -0
- package/dist/cjs/agent/registration.js +28 -0
- package/dist/cjs/agent/setup.d.ts +20 -0
- package/dist/cjs/agent/setup.js +229 -0
- package/dist/cjs/cli.js +101 -0
- package/dist/cjs/config/constants.d.ts +2 -0
- package/dist/cjs/config/constants.js +5 -1
- package/dist/cjs/config/pr-checkmate.template.json +11 -1
- package/dist/cjs/core/checks/dependencies/grype-scan.js +18 -0
- package/dist/cjs/core/checks/findings.js +33 -7
- package/dist/cjs/core/checks/git/large-files.d.ts +1 -0
- package/dist/cjs/core/checks/git/large-files.js +3 -3
- package/dist/cjs/core/checks/languages/lint.js +12 -0
- package/dist/cjs/core/checks/languages/ruff-shared.js +6 -1
- package/dist/cjs/core/checks/pr/commitlint.js +4 -2
- package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
- package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
- package/dist/cjs/core/checks/quality/package-size.d.ts +23 -1
- package/dist/cjs/core/checks/quality/package-size.js +20 -4
- package/dist/cjs/core/checks/quality/spellcheck.js +8 -5
- package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
- package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
- package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
- package/dist/cjs/core/checks/security/diff-security.js +21 -1
- package/dist/cjs/core/checks/security/security.d.ts +0 -13
- package/dist/cjs/core/checks/security/security.js +27 -0
- package/dist/cjs/core/checks/security/workflow-security.js +33 -0
- package/dist/cjs/core/commands.d.ts +6 -0
- package/dist/cjs/core/commands.js +11 -1
- package/dist/cjs/core/finding-store.d.ts +16 -0
- package/dist/cjs/core/finding-store.js +73 -0
- package/dist/cjs/core/install/doctor.js +12 -0
- package/dist/cjs/core/install/grype-db.d.ts +25 -0
- package/dist/cjs/core/install/grype-db.js +45 -0
- package/dist/cjs/core/install/profiles.d.ts +1 -1
- package/dist/cjs/core/install/profiles.js +1 -1
- package/dist/cjs/core/install/reuse.js +6 -3
- package/dist/cjs/core/registry.js +2 -0
- package/dist/cjs/core/reporters.js +9 -0
- package/dist/cjs/core/run-checks.js +15 -5
- package/dist/cjs/core/sarif.d.ts +13 -0
- package/dist/cjs/core/sarif.js +60 -0
- package/dist/cjs/core/types.d.ts +7 -2
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/init.d.ts +0 -8
- package/dist/cjs/init.js +12 -2
- package/dist/cjs/interfaces/index.d.ts +12 -0
- package/dist/cjs/mcp/config-tools.d.ts +30 -0
- package/dist/cjs/mcp/config-tools.js +320 -0
- package/dist/cjs/mcp/dependency.d.ts +33 -0
- package/dist/cjs/mcp/dependency.js +407 -0
- package/dist/cjs/mcp/protocol.d.ts +97 -0
- package/dist/cjs/mcp/protocol.js +181 -0
- package/dist/cjs/mcp/review.d.ts +63 -0
- package/dist/cjs/mcp/review.js +150 -0
- package/dist/cjs/mcp/serve.d.ts +8 -0
- package/dist/cjs/mcp/serve.js +49 -0
- package/dist/cjs/mcp/settings.d.ts +13 -0
- package/dist/cjs/mcp/settings.js +50 -0
- package/dist/cjs/mcp/tools.d.ts +23 -0
- package/dist/cjs/mcp/tools.js +633 -0
- package/dist/cjs/utils/execa.d.ts +7 -0
- package/dist/cjs/utils/execa.js +41 -1
- package/dist/cjs/utils/own-deps.js +8 -1
- package/dist/cjs/utils/resolve-bin.d.ts +2 -0
- package/dist/cjs/utils/resolve-bin.js +5 -0
- package/dist/cjs/utils/result-collector.d.ts +17 -1
- package/dist/cjs/utils/result-collector.js +7 -2
- package/dist/esm/agent/hook.d.ts +37 -0
- package/dist/esm/agent/hook.js +167 -0
- package/dist/esm/agent/registration.d.ts +10 -0
- package/dist/esm/agent/registration.js +21 -0
- package/dist/esm/agent/setup.d.ts +20 -0
- package/dist/esm/agent/setup.js +220 -0
- package/dist/esm/cli.js +100 -2
- package/dist/esm/config/constants.d.ts +2 -0
- package/dist/esm/config/constants.js +4 -0
- package/dist/esm/config/pr-checkmate.template.json +11 -1
- package/dist/esm/core/checks/dependencies/dependency-check.js +2 -2
- package/dist/esm/core/checks/dependencies/grype-scan.js +18 -0
- package/dist/esm/core/checks/dependencies/license-check.js +2 -2
- package/dist/esm/core/checks/dependencies/npm-audit.js +2 -2
- package/dist/esm/core/checks/findings.js +33 -7
- package/dist/esm/core/checks/git/diff-smell.js +1 -1
- package/dist/esm/core/checks/git/large-files.d.ts +1 -0
- package/dist/esm/core/checks/git/large-files.js +1 -1
- package/dist/esm/core/checks/languages/cpp-format.js +1 -1
- package/dist/esm/core/checks/languages/lint.js +12 -3
- package/dist/esm/core/checks/languages/prettier.js +3 -3
- package/dist/esm/core/checks/languages/python-format.js +2 -2
- package/dist/esm/core/checks/languages/python-lint.js +2 -2
- package/dist/esm/core/checks/languages/ruff-shared.js +6 -1
- package/dist/esm/core/checks/languages/typecheck.js +1 -1
- package/dist/esm/core/checks/pr/commitlint.js +4 -2
- package/dist/esm/core/checks/pr/pr-body.js +1 -1
- package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
- package/dist/esm/core/checks/pr/pr-size-check.js +3 -3
- package/dist/esm/core/checks/pr/pr-title-lint.js +1 -1
- package/dist/esm/core/checks/quality/dead-code.js +1 -1
- package/dist/esm/core/checks/quality/duplicate-check.js +1 -1
- package/dist/esm/core/checks/quality/markdown-lint.js +1 -1
- package/dist/esm/core/checks/quality/package-size.d.ts +23 -1
- package/dist/esm/core/checks/quality/package-size.js +19 -4
- package/dist/esm/core/checks/quality/spellcheck.js +11 -8
- package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
- package/dist/esm/core/checks/security/agent-instructions.js +146 -0
- package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
- package/dist/esm/core/checks/security/diff-security.js +21 -1
- package/dist/esm/core/checks/security/security.d.ts +0 -13
- package/dist/esm/core/checks/security/security.js +28 -1
- package/dist/esm/core/checks/security/workflow-security.js +33 -0
- package/dist/esm/core/commands.d.ts +6 -0
- package/dist/esm/core/commands.js +10 -0
- package/dist/esm/core/finding-store.d.ts +16 -0
- package/dist/esm/core/finding-store.js +67 -0
- package/dist/esm/core/install/doctor.js +12 -0
- package/dist/esm/core/install/grype-db.d.ts +25 -0
- package/dist/esm/core/install/grype-db.js +41 -0
- package/dist/esm/core/install/prepare.js +1 -1
- package/dist/esm/core/install/profiles.d.ts +1 -1
- package/dist/esm/core/install/profiles.js +1 -1
- package/dist/esm/core/install/reuse.js +6 -3
- package/dist/esm/core/registry.js +2 -0
- package/dist/esm/core/reporters.js +9 -0
- package/dist/esm/core/run-checks.js +17 -7
- package/dist/esm/core/sarif.d.ts +13 -0
- package/dist/esm/core/sarif.js +57 -0
- package/dist/esm/core/types.d.ts +7 -2
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/init.d.ts +0 -8
- package/dist/esm/init.js +15 -4
- package/dist/esm/interfaces/index.d.ts +12 -0
- package/dist/esm/mcp/config-tools.d.ts +30 -0
- package/dist/esm/mcp/config-tools.js +310 -0
- package/dist/esm/mcp/dependency.d.ts +33 -0
- package/dist/esm/mcp/dependency.js +402 -0
- package/dist/esm/mcp/protocol.d.ts +97 -0
- package/dist/esm/mcp/protocol.js +175 -0
- package/dist/esm/mcp/review.d.ts +63 -0
- package/dist/esm/mcp/review.js +139 -0
- package/dist/esm/mcp/serve.d.ts +8 -0
- package/dist/esm/mcp/serve.js +43 -0
- package/dist/esm/mcp/settings.d.ts +13 -0
- package/dist/esm/mcp/settings.js +43 -0
- package/dist/esm/mcp/tools.d.ts +23 -0
- package/dist/esm/mcp/tools.js +626 -0
- package/dist/esm/utils/execa.d.ts +7 -0
- package/dist/esm/utils/execa.js +40 -1
- package/dist/esm/utils/own-deps.js +8 -1
- package/dist/esm/utils/resolve-bin.d.ts +2 -0
- package/dist/esm/utils/resolve-bin.js +4 -0
- package/dist/esm/utils/result-collector.d.ts +17 -1
- package/dist/esm/utils/result-collector.js +7 -2
- package/eslint-default-rules.json +2 -8
- package/eslint.config.mjs +37 -8
- package/install-manifests/commitlint/package-lock.json +5 -4
- package/install-manifests/core/package-lock.json +5 -3
- package/install-manifests/js/package-lock.json +250 -571
- package/install-manifests/js/package.json +10 -8
- package/install-manifests/markdown/package-lock.json +16 -13
- package/install-manifests/markdown/package.json +3 -0
- package/install-manifests/spellcheck/package-lock.json +3 -3
- package/package.json +24 -16
- package/pr-checkmate.schema.json +35 -0
- package/src/config/.eslintrc.js +3 -9
- package/src/config/eslint-default-rules.json +2 -8
- package/src/config/pr-checkmate.schema.json +35 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tools and resources the MCP server offers.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, matching the two moments an agent can act on a standard:
|
|
5
|
+
*
|
|
6
|
+
* - **before writing** — `get_rules` says what this repository's gate enforces, so
|
|
7
|
+
* the code comes out right the first time;
|
|
8
|
+
* - **after writing** — `check_changes` and `scan_secrets` run the same checks CI
|
|
9
|
+
* will run, so the agent fixes findings in its own loop instead of in a review.
|
|
10
|
+
*
|
|
11
|
+
* All of it is read-only. Nothing here writes a file, posts a comment, or stages
|
|
12
|
+
* anything.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { loadProjectConfig } from '../config/loader.js';
|
|
17
|
+
import { DEFAULT_MAX_KB } from '../core/checks/git/large-files.js';
|
|
18
|
+
import { DEFAULT_MAX_FILES, DEFAULT_MAX_LINES } from '../core/checks/pr/pr-size-check.js';
|
|
19
|
+
import { COMMAND_TO_CHECK } from '../core/commands.js';
|
|
20
|
+
import { buildContext } from '../core/context.js';
|
|
21
|
+
import { REGISTRY } from '../core/registry.js';
|
|
22
|
+
import { findOwnPackageRoot } from '../utils/own-deps.js';
|
|
23
|
+
import { packageVersion } from '../utils/package-version.js';
|
|
24
|
+
import { explainConfig, formatSuggestion, suggestConfig, writeConfig } from './config-tools.js';
|
|
25
|
+
import { checkDependency, formatDependency } from './dependency.js';
|
|
26
|
+
import { fingerprintChanges, resolveRepoRoot, review, REVIEW_SCOPES, } from './review.js';
|
|
27
|
+
import { allowedTools, readMcpSettings } from './settings.js';
|
|
28
|
+
/** The checks that look for credentials and risky additions — what `scan_secrets` runs. */
|
|
29
|
+
const SECRET_CHECKS = ['Security Scan', 'Diff Security', 'Sensitive Files'];
|
|
30
|
+
/** Findings longer than this are cut: an agent's context is the scarce resource here. */
|
|
31
|
+
const MAX_DETAIL_CHARS = 3000;
|
|
32
|
+
/** Locations listed per check in the text; the structured result carries them all. */
|
|
33
|
+
const MAX_LOCATIONS_SHOWN = 10;
|
|
34
|
+
const RULES_URI = 'pr-checkmate://rules';
|
|
35
|
+
const INSTRUCTIONS = [
|
|
36
|
+
'pr-checkmate runs the same quality and security gate that CI applies to pull requests.',
|
|
37
|
+
'Before writing code in a repository, read the rules (get_rules or the pr-checkmate://rules resource).',
|
|
38
|
+
'While working, and before committing, call check_changes and fix every failed (❌) finding.',
|
|
39
|
+
'When you believe the work is done, call verify once — it runs the slow checks too — and fix what it reports before telling the user the work is finished.',
|
|
40
|
+
'Style findings (formatting, autofixable lint) are not worth fixing by hand: call apply_fixes, which rewrites only the files you changed.',
|
|
41
|
+
'To set pr-checkmate up for a repository, call suggest_config, show the user what it proposes, and only then call write_config; use explain_config to answer questions about a setting.',
|
|
42
|
+
'Before adding any package, call check_dependency: it catches names that do not exist, versions with known advisories and look-alike names.',
|
|
43
|
+
'Call scan_secrets before committing anything that touches configuration, environment files or credentials.',
|
|
44
|
+
'All tools are read-only.',
|
|
45
|
+
].join(' ');
|
|
46
|
+
/** Calls are queued: checks share process-wide state (cwd, tool lookup paths) and git. */
|
|
47
|
+
let queue = Promise.resolve();
|
|
48
|
+
function serialized(job) {
|
|
49
|
+
const run = queue.then(job, job);
|
|
50
|
+
queue = run.catch(() => undefined);
|
|
51
|
+
return run;
|
|
52
|
+
}
|
|
53
|
+
/** Run `job` with `dir` as the process working directory, restoring it afterwards. */
|
|
54
|
+
async function inDirectory(dir, job) {
|
|
55
|
+
const previous = process.cwd();
|
|
56
|
+
process.chdir(dir);
|
|
57
|
+
try {
|
|
58
|
+
return await job();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
process.chdir(previous);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The repository a call refers to. A globally installed server is started once and
|
|
66
|
+
* used from many projects, so each call may name its own directory; without one it
|
|
67
|
+
* is wherever the client launched us, which for Claude Code and most editors is the
|
|
68
|
+
* project the session is in.
|
|
69
|
+
*/
|
|
70
|
+
async function repoFor(args) {
|
|
71
|
+
const requested = typeof args.cwd === 'string' && args.cwd ? path.resolve(args.cwd) : process.cwd();
|
|
72
|
+
if (!fs.existsSync(requested) || !fs.statSync(requested).isDirectory()) {
|
|
73
|
+
throw new Error(`"${requested}" is not a directory`);
|
|
74
|
+
}
|
|
75
|
+
const root = await resolveRepoRoot(requested);
|
|
76
|
+
if (!root)
|
|
77
|
+
throw new Error(`"${requested}" is not inside a git repository`);
|
|
78
|
+
return root;
|
|
79
|
+
}
|
|
80
|
+
function clip(text) {
|
|
81
|
+
return text.length > MAX_DETAIL_CHARS
|
|
82
|
+
? `${text.slice(0, MAX_DETAIL_CHARS)}… [${text.length - MAX_DETAIL_CHARS} more characters cut]`
|
|
83
|
+
: text;
|
|
84
|
+
}
|
|
85
|
+
const ICON = {
|
|
86
|
+
pass: '✅',
|
|
87
|
+
warn: '⚠️',
|
|
88
|
+
fail: '❌',
|
|
89
|
+
skip: '⏭️',
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Render a report for a model: findings first, nothing for what passed.
|
|
93
|
+
*
|
|
94
|
+
* A passing check is the expected case and carries no information the agent can
|
|
95
|
+
* act on, so only the count survives. Skips stay because one of them can mean "the
|
|
96
|
+
* tooling never arrived" — a hole in coverage the agent should be able to see.
|
|
97
|
+
*/
|
|
98
|
+
export function formatReport(report, meta) {
|
|
99
|
+
const { passed, warned, failed, skipped } = report.summary;
|
|
100
|
+
const files = meta.changedFiles === undefined ? '' : `, ${meta.changedFiles} changed file(s)`;
|
|
101
|
+
const verdict = report.ok ? 'PASSED' : 'FAILED';
|
|
102
|
+
const lines = [
|
|
103
|
+
`pr-checkmate ${verdict} — ${failed} failed, ${warned} warned, ${passed} passed, ${skipped} skipped (scope: ${meta.scope}${files})`,
|
|
104
|
+
];
|
|
105
|
+
if (meta.note)
|
|
106
|
+
lines.push(`Note: ${meta.note}`);
|
|
107
|
+
const shown = report.results.filter(r => r.status !== 'pass');
|
|
108
|
+
for (const r of shown) {
|
|
109
|
+
lines.push(`${ICON[r.status]} ${r.name}${r.details ? ` — ${clip(r.details)}` : ''}`);
|
|
110
|
+
// Where, when the check knows. Capped: a hundred identical lines teach the agent
|
|
111
|
+
// nothing the first ten did not.
|
|
112
|
+
const where = r.findings ?? [];
|
|
113
|
+
for (const f of where.slice(0, MAX_LOCATIONS_SHOWN)) {
|
|
114
|
+
lines.push(` ${f.file}${f.line ? `:${f.line}` : ''} ${f.rule}${f.snippet ? ` — ${f.snippet}` : ''}`);
|
|
115
|
+
}
|
|
116
|
+
if (where.length > MAX_LOCATIONS_SHOWN) {
|
|
117
|
+
lines.push(` … and ${where.length - MAX_LOCATIONS_SHOWN} more`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// `advice` lets a caller that is not a tool call (the commit hook) say its own
|
|
121
|
+
// closing line, or none: "call this tool again" means nothing to a hook.
|
|
122
|
+
const advice = meta.advice ?? 'Fix the ❌ findings, then call this tool again to confirm.';
|
|
123
|
+
if (meta.changedFiles === 0)
|
|
124
|
+
lines.push('Nothing to review: the working tree matches HEAD.');
|
|
125
|
+
else if (!report.ok) {
|
|
126
|
+
if (advice)
|
|
127
|
+
lines.push(advice);
|
|
128
|
+
}
|
|
129
|
+
else if (warned > 0)
|
|
130
|
+
lines.push('Warnings are advisory and do not block the pull request.');
|
|
131
|
+
return {
|
|
132
|
+
text: lines.join('\n'),
|
|
133
|
+
structured: {
|
|
134
|
+
ok: report.ok,
|
|
135
|
+
scope: meta.scope,
|
|
136
|
+
summary: report.summary,
|
|
137
|
+
...(meta.changedFiles !== undefined && { changedFiles: meta.changedFiles }),
|
|
138
|
+
findings: shown.map(r => ({
|
|
139
|
+
check: r.name,
|
|
140
|
+
status: r.status,
|
|
141
|
+
...(r.details && { details: clip(r.details) }),
|
|
142
|
+
...(r.findings && r.findings.length > 0 && { locations: r.findings }),
|
|
143
|
+
})),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** The CLI spelling of a check, so the rules can say "run `pr-checkmate lint`". */
|
|
148
|
+
function commandFor(checkName) {
|
|
149
|
+
return Object.entries(COMMAND_TO_CHECK).find(([, name]) => name === checkName)?.[0];
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The numeric limits that, once crossed, produce a finding — read from the live
|
|
153
|
+
* config so the agent can size its change before writing it instead of learning the
|
|
154
|
+
* ceiling from a failure. Only enabled checks are listed: a limit nobody enforces is
|
|
155
|
+
* not a rule.
|
|
156
|
+
*/
|
|
157
|
+
function describeLimits(config) {
|
|
158
|
+
const c = config;
|
|
159
|
+
const on = (key) => c[key] && c[key]?.enabled !== false ? c[key] : undefined;
|
|
160
|
+
const lines = [];
|
|
161
|
+
// The built-in default fills in when the repository sets nothing: the limit
|
|
162
|
+
// applies either way, and an agent should not have to guess it.
|
|
163
|
+
const size = c.prSize;
|
|
164
|
+
if (size?.enabled !== false) {
|
|
165
|
+
lines.push(`- A pull request: at most ${size?.maxFiles ?? DEFAULT_MAX_FILES} files and ${size?.maxLines ?? DEFAULT_MAX_LINES} changed lines.`);
|
|
166
|
+
}
|
|
167
|
+
const large = c.largeFiles;
|
|
168
|
+
if (large?.enabled !== false) {
|
|
169
|
+
lines.push(`- A committed file: at most ${large?.maxSizeKb ?? DEFAULT_MAX_KB} KB.`);
|
|
170
|
+
}
|
|
171
|
+
const dup = on('duplicate');
|
|
172
|
+
if (dup)
|
|
173
|
+
lines.push(`- Copy-pasted code: ${dup.minLines}+ identical lines (${dup.minTokens}+ tokens) is reported as a duplicate.`);
|
|
174
|
+
const body = on('prBody');
|
|
175
|
+
if (body) {
|
|
176
|
+
lines.push(`- Pull request description: at least ${body.minLength} characters${body.requireTicket ? ' and a ticket reference' : ''}.`);
|
|
177
|
+
}
|
|
178
|
+
if (on('todoFixme')?.requireTicket)
|
|
179
|
+
lines.push('- A TODO/FIXME needs a ticket reference.');
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The ESLint rules the gate runs, as the repository will actually see them: the
|
|
184
|
+
* bundled defaults with the repository's own `eslint.rules` merged over them, the
|
|
185
|
+
* same order the real config applies. Only `error` is listed — a warning does not
|
|
186
|
+
* fail the build, and a hundred of them would bury the ones that do.
|
|
187
|
+
*/
|
|
188
|
+
function describeLintRules(config) {
|
|
189
|
+
let defaults = {};
|
|
190
|
+
const root = findOwnPackageRoot();
|
|
191
|
+
try {
|
|
192
|
+
if (root) {
|
|
193
|
+
defaults = JSON.parse(fs.readFileSync(path.join(root, 'eslint-default-rules.json'), 'utf-8'));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Without the defaults file only the repository's own rules can be listed.
|
|
198
|
+
}
|
|
199
|
+
// The real config reads `eslint.rules`; the typed interface still names it `lint`.
|
|
200
|
+
const own = config.eslint?.rules;
|
|
201
|
+
const merged = { ...defaults, ...(own ?? config.lint?.rules ?? {}) };
|
|
202
|
+
const level = (setting) => {
|
|
203
|
+
const head = Array.isArray(setting) ? setting[0] : setting;
|
|
204
|
+
return head === 2 ? 'error' : head === 1 ? 'warn' : head === 0 ? 'off' : String(head);
|
|
205
|
+
};
|
|
206
|
+
const failing = Object.entries(merged)
|
|
207
|
+
.filter(([, setting]) => level(setting) === 'error')
|
|
208
|
+
.map(([rule]) => `\`${rule}\``);
|
|
209
|
+
return failing.length > 0 ? [failing.join(', ')] : [];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Describe what this repository's gate enforces, in the order an agent needs it.
|
|
213
|
+
*
|
|
214
|
+
* Built from the live registry and the repository's own `pr-checkmate.json`, so it
|
|
215
|
+
* cannot drift from what CI will do — a hand-written rules file would, the first
|
|
216
|
+
* time someone promoted a check to a gate.
|
|
217
|
+
*/
|
|
218
|
+
async function describeRules(cwd) {
|
|
219
|
+
return inDirectory(cwd, async () => {
|
|
220
|
+
const config = loadProjectConfig(cwd);
|
|
221
|
+
// `full` skips every git call: applicability depends on languages and config,
|
|
222
|
+
// not on what is in the diff.
|
|
223
|
+
const ctx = await buildContext({ cwd, config, full: true, reporter: 'none' });
|
|
224
|
+
const severity = config.severity ?? {};
|
|
225
|
+
const gate = [];
|
|
226
|
+
const advisory = [];
|
|
227
|
+
for (const check of REGISTRY) {
|
|
228
|
+
if (severity[check.name] === 'off' || !(await check.applies(ctx)))
|
|
229
|
+
continue;
|
|
230
|
+
const blocking = severity[check.name] === 'error' ||
|
|
231
|
+
(check.phase === 'blocking' && severity[check.name] !== 'warn');
|
|
232
|
+
const cmd = commandFor(check.name);
|
|
233
|
+
const entry = `- ${check.name}${cmd ? ` (\`pr-checkmate ${cmd}\`)` : ''}`;
|
|
234
|
+
(blocking ? gate : advisory).push(entry);
|
|
235
|
+
}
|
|
236
|
+
const out = [
|
|
237
|
+
`# pr-checkmate rules for ${path.basename(cwd)}`,
|
|
238
|
+
'',
|
|
239
|
+
`Languages detected: ${ctx.languages.join(', ') || 'none'}`,
|
|
240
|
+
'',
|
|
241
|
+
'## Blocking — a finding here fails the pull request',
|
|
242
|
+
...(gate.length > 0 ? gate : ['- none']),
|
|
243
|
+
'',
|
|
244
|
+
'## Advisory — reported, never blocks',
|
|
245
|
+
...(advisory.length > 0 ? advisory : ['- none']),
|
|
246
|
+
];
|
|
247
|
+
if (config.customRules?.length) {
|
|
248
|
+
out.push('', '## Project rules (matched against added lines)');
|
|
249
|
+
for (const rule of config.customRules) {
|
|
250
|
+
out.push(`- ${rule.name} [${rule.severity ?? 'warn'}]${rule.message ? `: ${rule.message}` : ''}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (config.bannedImports?.length) {
|
|
254
|
+
out.push('', '## Banned imports and dependencies');
|
|
255
|
+
for (const banned of config.bannedImports) {
|
|
256
|
+
out.push(`- ${banned.name} [${banned.severity ?? 'error'}]${banned.message ? `: ${banned.message}` : ''}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const limits = describeLimits(config);
|
|
260
|
+
if (limits.length > 0)
|
|
261
|
+
out.push('', '## Limits to stay inside', ...limits);
|
|
262
|
+
const lint = describeLintRules(config);
|
|
263
|
+
if (lint.length > 0)
|
|
264
|
+
out.push('', '## Lint rules that fail the build', ...lint);
|
|
265
|
+
out.push('', '## Workflow', '1. Write code that follows the rules above.', '2. Call `check_changes` as you work; fix every ❌ and call it again.', '3. Call `scan_secrets` before committing configuration, environment or credential files, and `check_dependency` before adding any package.', '4. Call `verify` once when the work is done — it adds the slow checks (typecheck, dependency audit) — before telling the user it is finished.');
|
|
266
|
+
return out.join('\n');
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const CWD_PROPERTY = {
|
|
270
|
+
cwd: {
|
|
271
|
+
type: 'string',
|
|
272
|
+
description: 'Directory inside the repository to review. Defaults to the directory the server was started in.',
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const getRules = {
|
|
276
|
+
name: 'get_rules',
|
|
277
|
+
description: "Describe the quality and security rules this repository's gate enforces: which checks block a pull request, which only warn, and the project's own custom rules and banned imports. Read it before writing code so the first draft already conforms.",
|
|
278
|
+
inputSchema: { type: 'object', properties: { ...CWD_PROPERTY }, additionalProperties: false },
|
|
279
|
+
async run(args) {
|
|
280
|
+
const cwd = await repoFor(args);
|
|
281
|
+
return serialized(async () => ({ text: await describeRules(cwd) }));
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
const checkChanges = {
|
|
285
|
+
name: 'check_changes',
|
|
286
|
+
description: "Run the repository's pull-request gate on the code you have changed — uncommitted files included, nothing needs to be staged — and return the findings. " +
|
|
287
|
+
'Call it after finishing a change and before committing; fix every failed finding and call it again. ' +
|
|
288
|
+
'Fast checks only by default; pass deep: true for typecheck, dependency audit and the other slow ones.',
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
scope: {
|
|
293
|
+
type: 'string',
|
|
294
|
+
enum: [...REVIEW_SCOPES],
|
|
295
|
+
description: '"worktree" (default): all uncommitted changes. "staged": what git commit would record. "branch": commits since the default branch. "full": every tracked file.',
|
|
296
|
+
},
|
|
297
|
+
deep: {
|
|
298
|
+
type: 'boolean',
|
|
299
|
+
description: 'Also run heavy and network checks. Slower; use before opening a pull request.',
|
|
300
|
+
},
|
|
301
|
+
checks: {
|
|
302
|
+
type: 'array',
|
|
303
|
+
items: { type: 'string' },
|
|
304
|
+
description: 'Run only these checks, by command name (e.g. "lint", "security") or display name (e.g. "ESLint").',
|
|
305
|
+
},
|
|
306
|
+
...CWD_PROPERTY,
|
|
307
|
+
},
|
|
308
|
+
additionalProperties: false,
|
|
309
|
+
},
|
|
310
|
+
async run(args) {
|
|
311
|
+
const scope = args.scope ?? 'worktree';
|
|
312
|
+
if (!REVIEW_SCOPES.includes(scope)) {
|
|
313
|
+
return {
|
|
314
|
+
text: `Unknown scope "${String(scope)}". Use one of: ${REVIEW_SCOPES.join(', ')}.`,
|
|
315
|
+
isError: true,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const requested = Array.isArray(args.checks) ? args.checks.map(String) : [];
|
|
319
|
+
const known = new Set(REGISTRY.map(c => c.name));
|
|
320
|
+
const only = requested.map(name => COMMAND_TO_CHECK[name] ?? name);
|
|
321
|
+
const unknown = requested.filter((_, i) => !known.has(only[i]));
|
|
322
|
+
// A misspelt name must never shrink the run to nothing and report green — the
|
|
323
|
+
// same rule the CLI applies, for the same reason.
|
|
324
|
+
if (unknown.length > 0) {
|
|
325
|
+
return {
|
|
326
|
+
text: `Unknown check(s): ${unknown.join(', ')}. Valid command names: ${Object.keys(COMMAND_TO_CHECK).join(', ')}.`,
|
|
327
|
+
isError: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const cwd = await repoFor(args);
|
|
331
|
+
const outcome = await serialized(() => inDirectory(cwd, () => review({ cwd, scope: scope, only, deep: args.deep === true })));
|
|
332
|
+
return formatReport(outcome.report, outcome);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
const scanSecrets = {
|
|
336
|
+
name: 'scan_secrets',
|
|
337
|
+
description: 'Scan the code you have changed for committed credentials, tokens, private keys and risky additions. Run it before committing configuration, environment or credential files, and whenever a value looks like a secret.',
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {
|
|
341
|
+
scope: {
|
|
342
|
+
type: 'string',
|
|
343
|
+
enum: [...REVIEW_SCOPES],
|
|
344
|
+
description: 'Same meaning as in check_changes. Defaults to "worktree".',
|
|
345
|
+
},
|
|
346
|
+
...CWD_PROPERTY,
|
|
347
|
+
},
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
},
|
|
350
|
+
async run(args) {
|
|
351
|
+
const scope = args.scope ?? 'worktree';
|
|
352
|
+
if (!REVIEW_SCOPES.includes(scope)) {
|
|
353
|
+
return {
|
|
354
|
+
text: `Unknown scope "${String(scope)}". Use one of: ${REVIEW_SCOPES.join(', ')}.`,
|
|
355
|
+
isError: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const cwd = await repoFor(args);
|
|
359
|
+
const outcome = await serialized(() => inDirectory(cwd, () => review({ cwd, scope: scope, only: [...SECRET_CHECKS] })));
|
|
360
|
+
return formatReport(outcome.report, outcome);
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
const verify = {
|
|
364
|
+
name: 'verify',
|
|
365
|
+
description: 'The final gate. Runs every check the pull-request gate runs, the slow ones included (typecheck, dependency audit, spellcheck, dead code), on the code you have changed. ' +
|
|
366
|
+
'Call it once when you believe the work is done and before you tell the user it is finished. ' +
|
|
367
|
+
'check_changes is the quick loop while you work; a clean verify is the closest you can get to knowing CI will agree.',
|
|
368
|
+
inputSchema: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
scope: {
|
|
372
|
+
type: 'string',
|
|
373
|
+
enum: [...REVIEW_SCOPES],
|
|
374
|
+
description: 'Same meaning as in check_changes. Defaults to "worktree".',
|
|
375
|
+
},
|
|
376
|
+
...CWD_PROPERTY,
|
|
377
|
+
},
|
|
378
|
+
additionalProperties: false,
|
|
379
|
+
},
|
|
380
|
+
async run(args) {
|
|
381
|
+
const scope = args.scope ?? 'worktree';
|
|
382
|
+
if (!REVIEW_SCOPES.includes(scope)) {
|
|
383
|
+
return {
|
|
384
|
+
text: `Unknown scope "${String(scope)}". Use one of: ${REVIEW_SCOPES.join(', ')}.`,
|
|
385
|
+
isError: true,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const cwd = await repoFor(args);
|
|
389
|
+
const outcome = await serialized(() => inDirectory(cwd, () => review({ cwd, scope: scope, deep: true })));
|
|
390
|
+
return formatReport(outcome.report, outcome);
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
/** The only checks that can rewrite code safely: formatting and lint autofixes. */
|
|
394
|
+
const FIXERS = ['Prettier', 'ESLint'];
|
|
395
|
+
const applyFixes = {
|
|
396
|
+
name: 'apply_fixes',
|
|
397
|
+
writes: true,
|
|
398
|
+
description: 'Apply the safe automatic fixes — Prettier formatting and ESLint --fix — to the files you have changed, and nothing else. ' +
|
|
399
|
+
'Use it instead of spending effort fixing style findings by hand. It rewrites files in place, reports exactly which ones it changed, and then re-checks, so what is left is what needs a human decision. ' +
|
|
400
|
+
'It touches only files in your current changes; it never reformats the rest of the repository.',
|
|
401
|
+
inputSchema: { type: 'object', properties: { ...CWD_PROPERTY }, additionalProperties: false },
|
|
402
|
+
async run(args) {
|
|
403
|
+
const cwd = await repoFor(args);
|
|
404
|
+
return serialized(() => inDirectory(cwd, async () => {
|
|
405
|
+
const before = await fingerprintChanges(cwd);
|
|
406
|
+
if (before.size === 0)
|
|
407
|
+
return { text: 'Nothing to fix: the working tree matches HEAD.' };
|
|
408
|
+
await review({ cwd, scope: 'worktree', only: [...FIXERS], write: true });
|
|
409
|
+
const after = await fingerprintChanges(cwd);
|
|
410
|
+
const changed = [...after]
|
|
411
|
+
.filter(([file, hash]) => before.get(file) !== hash)
|
|
412
|
+
.map(([file]) => file);
|
|
413
|
+
// Re-check read-only: a fixer reporting success says nothing about what it could not fix.
|
|
414
|
+
const left = await review({ cwd, scope: 'worktree', only: [...FIXERS] });
|
|
415
|
+
const remaining = formatReport(left.report, left);
|
|
416
|
+
const head = changed.length > 0
|
|
417
|
+
? `Rewrote ${changed.length} file(s):\n${changed.map(f => ` ${f}`).join('\n')}`
|
|
418
|
+
: 'No file needed fixing.';
|
|
419
|
+
return {
|
|
420
|
+
text: `${head}\n\nAfter the fixes:\n${remaining.text}`,
|
|
421
|
+
structured: { changedFiles: changed, ...(remaining.structured ?? {}) },
|
|
422
|
+
};
|
|
423
|
+
}));
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
const suggestConfigTool = {
|
|
427
|
+
name: 'suggest_config',
|
|
428
|
+
description: 'Set pr-checkmate up for THIS repository. Detects its languages, runs the fast checks across the whole repository as a baseline, and proposes a pr-checkmate.json: ' +
|
|
429
|
+
'checks that already fail are proposed as warnings so existing debt does not block every pull request, generated directories are excluded, and everything else keeps its defaults. ' +
|
|
430
|
+
'It writes nothing: show the proposal to the user, then call write_config if they agree. May take a while on a large repository.',
|
|
431
|
+
inputSchema: { type: 'object', properties: { ...CWD_PROPERTY }, additionalProperties: false },
|
|
432
|
+
async run(args) {
|
|
433
|
+
const cwd = await repoFor(args);
|
|
434
|
+
return formatSuggestion(await serialized(() => inDirectory(cwd, () => suggestConfig(cwd))));
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
const writeConfigTool = {
|
|
438
|
+
name: 'write_config',
|
|
439
|
+
writes: true,
|
|
440
|
+
description: "Write settings into the repository's pr-checkmate.json. " +
|
|
441
|
+
'The config is validated first (unknown keys, misspelt check names and bad values are refused with a suggestion), ' +
|
|
442
|
+
'then merged over what is already there: a value the user has already set is never replaced unless overwrite is true, ' +
|
|
443
|
+
'and the call lists what it kept. ' +
|
|
444
|
+
'Call it only after the user has seen and agreed to the settings.',
|
|
445
|
+
inputSchema: {
|
|
446
|
+
type: 'object',
|
|
447
|
+
properties: {
|
|
448
|
+
config: {
|
|
449
|
+
type: 'object',
|
|
450
|
+
description: 'The settings to add, e.g. {"severity": {"ESLint": "warn"}, "prSize": {"maxFiles": 80}}.',
|
|
451
|
+
},
|
|
452
|
+
overwrite: {
|
|
453
|
+
type: 'boolean',
|
|
454
|
+
description: 'Replace values that are already set. Only when the user asked to change them.',
|
|
455
|
+
},
|
|
456
|
+
...CWD_PROPERTY,
|
|
457
|
+
},
|
|
458
|
+
required: ['config'],
|
|
459
|
+
additionalProperties: false,
|
|
460
|
+
},
|
|
461
|
+
async run(args) {
|
|
462
|
+
const cwd = await repoFor(args);
|
|
463
|
+
return writeConfig(cwd, args.config, args.overwrite === true);
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
const explainConfigTool = {
|
|
467
|
+
name: 'explain_config',
|
|
468
|
+
description: 'Explain a pr-checkmate.json setting from the schema that ships with this version: what it does, its default, its allowed values and its sub-keys. Call it with no key to list every top-level setting. Use it to answer "how do I make X stricter / turn Y off" before changing anything.',
|
|
469
|
+
inputSchema: {
|
|
470
|
+
type: 'object',
|
|
471
|
+
properties: {
|
|
472
|
+
key: {
|
|
473
|
+
type: 'string',
|
|
474
|
+
description: 'A setting, with dots for nested ones: "prSize" or "prSize.maxLines". Omit to list all.',
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
additionalProperties: false,
|
|
478
|
+
},
|
|
479
|
+
async run(args) {
|
|
480
|
+
return explainConfig(typeof args.key === 'string' && args.key ? args.key : undefined);
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
const dependencyTool = {
|
|
484
|
+
name: 'check_dependency',
|
|
485
|
+
network: true,
|
|
486
|
+
description: 'Before adding a package, ask whether it should be added.' +
|
|
487
|
+
'Reports whether it exists (models invent plausible names that attackers then publish), whether the version exists, known advisories, how new and how used it is, install-time scripts, deprecation, and whether the name imitates a popular package. ' +
|
|
488
|
+
'Only the package name and version are sent to the public registry and advisory database; ' +
|
|
489
|
+
'nothing from the repository leaves the machine. ' +
|
|
490
|
+
'A verdict of unknown means a lookup failed, never that the package is safe.',
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
properties: {
|
|
494
|
+
name: {
|
|
495
|
+
type: 'string',
|
|
496
|
+
description: 'Package name exactly as it would be installed, e.g. "left-pad" or "@scope/pkg".',
|
|
497
|
+
},
|
|
498
|
+
version: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
description: 'Exact version you intend to add. Omit to check the latest.',
|
|
501
|
+
},
|
|
502
|
+
ecosystem: { type: 'string', enum: ['npm', 'pypi'], description: 'Defaults to npm.' },
|
|
503
|
+
},
|
|
504
|
+
required: ['name'],
|
|
505
|
+
additionalProperties: false,
|
|
506
|
+
},
|
|
507
|
+
async run(args) {
|
|
508
|
+
if (typeof args.name !== 'string' || !args.name) {
|
|
509
|
+
return { text: 'check_dependency needs a package name.', isError: true };
|
|
510
|
+
}
|
|
511
|
+
if (args.ecosystem !== undefined && args.ecosystem !== 'npm' && args.ecosystem !== 'pypi') {
|
|
512
|
+
return { text: 'ecosystem must be "npm" or "pypi".', isError: true };
|
|
513
|
+
}
|
|
514
|
+
const report = await checkDependency(args.name, {
|
|
515
|
+
version: typeof args.version === 'string' ? args.version : undefined,
|
|
516
|
+
ecosystem: args.ecosystem,
|
|
517
|
+
});
|
|
518
|
+
return { text: formatDependency(report), structured: { ...report } };
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
const rulesResource = {
|
|
522
|
+
uri: RULES_URI,
|
|
523
|
+
name: 'Repository rules',
|
|
524
|
+
description: "What this repository's pull-request gate enforces, from its live configuration.",
|
|
525
|
+
mimeType: 'text/markdown',
|
|
526
|
+
async read() {
|
|
527
|
+
const cwd = await repoFor({});
|
|
528
|
+
return serialized(() => describeRules(cwd));
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
const PROMPTS = [
|
|
532
|
+
{
|
|
533
|
+
name: 'prepare_pr',
|
|
534
|
+
description: 'Get the current work ready for a pull request: fix style, run the full gate, report what is left.',
|
|
535
|
+
render: () => [
|
|
536
|
+
'Get my current changes ready for a pull request using the pr-checkmate tools.',
|
|
537
|
+
'1. Call apply_fixes to clear formatting and autofixable lint.',
|
|
538
|
+
'2. Call verify. It runs every check, including the slow ones.',
|
|
539
|
+
'3. Fix each failed (❌) finding in the code. Do not silence a rule, edit pr-checkmate.json, or skip a hook to make it pass.',
|
|
540
|
+
'4. Call verify again until nothing fails.',
|
|
541
|
+
'5. Tell me what is left: warnings I should know about, and anything you could not fix and why.',
|
|
542
|
+
'Do not commit unless I ask.',
|
|
543
|
+
].join('\n'),
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: 'fix_findings',
|
|
547
|
+
description: 'Work through everything the gate reports, security problems first.',
|
|
548
|
+
render: () => [
|
|
549
|
+
'Call check_changes with deep: true and fix what it reports, in this order:',
|
|
550
|
+
'1. Security Scan and Diff Security: a leaked secret must be removed from the code and I must be told to rotate it.',
|
|
551
|
+
'2. Every other failed (❌) check.',
|
|
552
|
+
'3. Warnings, only where the fix is small and obvious.',
|
|
553
|
+
'After each group call check_changes again. Fix the cause in the code; never silence the rule. If a finding is a false positive, say so and ask me before ignoring it.',
|
|
554
|
+
].join('\n'),
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: 'explain_finding',
|
|
558
|
+
description: 'Explain a finding or rule in plain terms and show how to fix it.',
|
|
559
|
+
arguments: [
|
|
560
|
+
{
|
|
561
|
+
name: 'finding',
|
|
562
|
+
description: 'The finding or check name, as the report shows it.',
|
|
563
|
+
required: true,
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
render: ({ finding }) => `Explain this pr-checkmate finding: "${finding}". Say what it protects against, why it matters in this repository, and the smallest change that fixes it. Use explain_config if it is a configurable setting. Do not change any file unless I ask.`,
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
name: 'setup_pr_checkmate',
|
|
570
|
+
description: 'Configure pr-checkmate for this repository: propose settings, then write them once approved.',
|
|
571
|
+
render: () => [
|
|
572
|
+
'Set up pr-checkmate for this repository.',
|
|
573
|
+
'1. Call suggest_config and show me what it found and what it proposes, in plain language.',
|
|
574
|
+
'2. Wait for my answer. Do not write anything before I agree.',
|
|
575
|
+
'3. Call write_config with the settings I approved. Use explain_config if I ask what a setting does.',
|
|
576
|
+
'4. Call check_changes with scope "full" and tell me how many checks are red, so I know where we start.',
|
|
577
|
+
'Explain that checks proposed as warnings should be promoted back to errors one by one as they go clean.',
|
|
578
|
+
].join('\n'),
|
|
579
|
+
},
|
|
580
|
+
];
|
|
581
|
+
const ALL_TOOLS = [
|
|
582
|
+
getRules,
|
|
583
|
+
checkChanges,
|
|
584
|
+
applyFixes,
|
|
585
|
+
scanSecrets,
|
|
586
|
+
dependencyTool,
|
|
587
|
+
suggestConfigTool,
|
|
588
|
+
writeConfigTool,
|
|
589
|
+
explainConfigTool,
|
|
590
|
+
verify,
|
|
591
|
+
];
|
|
592
|
+
const DISABLED_INSTRUCTIONS = 'pr-checkmate is turned off for this project (pr-checkmate.json, "mcp": { "enabled": false }). It has no tools here; do not look for them.';
|
|
593
|
+
/** What the model is told on connect, with a note for any tool the project took away. */
|
|
594
|
+
function instructionsFor(settings, offered) {
|
|
595
|
+
if (!settings.enabled)
|
|
596
|
+
return DISABLED_INSTRUCTIONS;
|
|
597
|
+
const hidden = ALL_TOOLS.filter(t => !offered.includes(t)).map(t => t.name);
|
|
598
|
+
return hidden.length === 0
|
|
599
|
+
? INSTRUCTIONS
|
|
600
|
+
: `${INSTRUCTIONS}\nThis project does not offer: ${hidden.join(', ')}. Skip any step above that needs one of them.`;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The server definition `serve` hands to the protocol layer.
|
|
604
|
+
*
|
|
605
|
+
* What it offers is decided per access, from the project's `mcp` settings, so a
|
|
606
|
+
* change to `pr-checkmate.json` takes effect on the agent's next request.
|
|
607
|
+
*/
|
|
608
|
+
export function createServerDefinition(cwd = process.cwd()) {
|
|
609
|
+
return {
|
|
610
|
+
name: 'pr-checkmate',
|
|
611
|
+
version: packageVersion(),
|
|
612
|
+
get instructions() {
|
|
613
|
+
const settings = readMcpSettings(cwd);
|
|
614
|
+
return instructionsFor(settings, allowedTools(ALL_TOOLS, settings));
|
|
615
|
+
},
|
|
616
|
+
get tools() {
|
|
617
|
+
return allowedTools(ALL_TOOLS, readMcpSettings(cwd));
|
|
618
|
+
},
|
|
619
|
+
get resources() {
|
|
620
|
+
return readMcpSettings(cwd).enabled ? [rulesResource] : [];
|
|
621
|
+
},
|
|
622
|
+
get prompts() {
|
|
623
|
+
return readMcpSettings(cwd).enabled ? PROMPTS : [];
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* dragging the subprocess toolchain into consumers that never spawn a process.
|
|
14
14
|
*/
|
|
15
15
|
import type { Options, Result } from 'execa';
|
|
16
|
+
/**
|
|
17
|
+
* Keep children that would inherit our streams off them, and stop them sharing
|
|
18
|
+
* stdin. `stderr` (the MCP server) keeps their output as the server's log;
|
|
19
|
+
* `discard` (the commit hook) drops it, because a hook's stderr is read by the
|
|
20
|
+
* model and tool banners are not what it needs to hear. No other command is affected.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isolateInheritedStdio(output?: 'stderr' | 'discard'): void;
|
|
16
23
|
/**
|
|
17
24
|
* Run a command via execa, loading the module lazily on first call.
|
|
18
25
|
*
|