activate-agentmd 2.4.0 → 2.5.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/README.md +11 -1
- package/bin/agentmd.js +9 -2
- package/package.json +1 -1
- package/src/commands/bundle.js +151 -0
- package/src/commands/ci.js +6 -1
- package/src/commands/lint.js +84 -0
- package/src/commands/pins.js +43 -0
- package/src/commands/review.js +4 -2
- package/src/commands/test.js +37 -17
- package/src/detect.json +1 -0
- package/src/lib/agents.js +18 -55
- package/src/lib/analytics.js +6 -0
- package/src/lib/bundle.js +169 -0
- package/src/lib/enterprise.js +7 -1
- package/src/lib/license.js +15 -2
- package/src/lib/lint-text.js +150 -0
- package/src/lib/lint.js +213 -0
- package/src/lib/manifest.js +8 -0
- package/src/lib/patterns.js +11 -14
- package/src/lib/pro.js +1 -1
- package/src/lib/telemetry.js +1 -1
- package/src/lib/versions.js +56 -103
- package/src/pins.json +403 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# activate-agentmd
|
|
2
2
|
|
|
3
|
-
> The package manager for AI coding-agent engineering standards
|
|
3
|
+
> The package manager for AI coding-agent engineering standards — and the
|
|
4
|
+
> checks that keep what your agent reads trustworthy.
|
|
4
5
|
|
|
5
6
|
```bash
|
|
6
7
|
npx activate-agentmd init
|
|
@@ -62,6 +63,9 @@ content is never touched.
|
|
|
62
63
|
| `update` | Pull current versions |
|
|
63
64
|
| `validate` | Check registry + manifest integrity |
|
|
64
65
|
| `agents` | List supported agent config targets |
|
|
66
|
+
| `lint` | Check every file your agent reads: leaked keys, prompt injection, bloat, stale pins. Exit 1 on an error; `--sarif` for code scanning |
|
|
67
|
+
| `pins` | The breaking changes for the dependency majors this project runs |
|
|
68
|
+
| `bundle sign\|verify\|attest` | Sign the exact set of files your agent reads with your org's key; fail CI on any unsigned change |
|
|
65
69
|
|
|
66
70
|
Package paths are `<model>/<category>/<package>`. A trailing slash installs the
|
|
67
71
|
whole category:
|
|
@@ -71,6 +75,12 @@ agentmd install claude/Security/owasp # one
|
|
|
71
75
|
agentmd install claude/Testing/ # all 11
|
|
72
76
|
```
|
|
73
77
|
|
|
78
|
+
`lint` works on any repository, no install or manifest needed:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx activate-agentmd lint
|
|
82
|
+
```
|
|
83
|
+
|
|
74
84
|
Full reference:
|
|
75
85
|
[docs/cli.md](https://github.com/Aaditya1273/Agent.md/blob/main/docs/cli.md)
|
|
76
86
|
|
package/bin/agentmd.js
CHANGED
|
@@ -31,6 +31,9 @@ ${pc.bold("GETTING STARTED")}
|
|
|
31
31
|
${pc.cyan("init")} Scan this project and install what matches
|
|
32
32
|
${pc.cyan("link")} Wire installed packages into your agent config files
|
|
33
33
|
${pc.cyan("extract")} ${pc.yellow("PRO")} Derive this repo's unwritten conventions into a local standard
|
|
34
|
+
${pc.cyan("lint")} Check every file your agent reads: secrets, prompt injection, bloat, stale pins
|
|
35
|
+
${pc.cyan("pins")} Print the breaking changes for the dependency majors this project runs
|
|
36
|
+
${pc.cyan("bundle")} ${pc.white("<sign|verify|attest>")} Sign and verify the exact set of files your agent reads (keygen to start)
|
|
34
37
|
${pc.cyan("review")} ${pc.white("[--base ref]")} Check a diff against the installed standards (--fast: offline patterns, --local: Ollama)
|
|
35
38
|
${pc.cyan("test")} Benchmark whether the model actually obeys the installed standards
|
|
36
39
|
|
|
@@ -73,7 +76,7 @@ ${pc.bold("OPTIONS")}
|
|
|
73
76
|
${pc.bold("FREE AND PRO")}
|
|
74
77
|
${pc.dim("Everything local is free, forever: init, link, install, update, list, search,")}
|
|
75
78
|
${pc.dim("review --fast and review --local. No account, no key, no telemetry.")}
|
|
76
|
-
${pc.dim("Pro adds extract, private sync, analytics and cloud CI — " + (process.env.AGENTMD_PRO_URL || "https://
|
|
79
|
+
${pc.dim("Pro adds extract, private sync, analytics and cloud CI — " + (process.env.AGENTMD_PRO_URL || "https://agentmd.pages.dev/pro"))}
|
|
77
80
|
${pc.dim("In CI, set AGENTMD_PRO_KEY instead of logging in.")}
|
|
78
81
|
|
|
79
82
|
${pc.bold("EXAMPLES")}
|
|
@@ -109,6 +112,9 @@ const COMMANDS = {
|
|
|
109
112
|
validate: () => require("../src/commands/validate").validate,
|
|
110
113
|
agents: () => require("../src/commands/agents").agents,
|
|
111
114
|
review: () => require("../src/commands/review").review,
|
|
115
|
+
lint: () => require("../src/commands/lint").lint,
|
|
116
|
+
pins: () => require("../src/commands/pins").pins,
|
|
117
|
+
bundle: () => require("../src/commands/bundle").bundle,
|
|
112
118
|
test: () => require("../src/commands/test").test,
|
|
113
119
|
ci: () => require("../src/commands/ci").ci,
|
|
114
120
|
|
|
@@ -140,7 +146,8 @@ const ALIASES = {
|
|
|
140
146
|
up: "update",
|
|
141
147
|
upgrade: "update",
|
|
142
148
|
check: "outdated",
|
|
143
|
-
lint:
|
|
149
|
+
// `lint` used to alias `validate`. It is a command of its own now: it
|
|
150
|
+
// checks the user's instruction files, where `validate` checks the registry.
|
|
144
151
|
};
|
|
145
152
|
|
|
146
153
|
async function main() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "activate-agentmd",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "The package manager for AI coding-agent engineering standards. Install, link and update rules across Claude Code, Cursor, Codex, Gemini and Copilot.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bundle.js
|
|
3
|
+
*
|
|
4
|
+
* `agentmd bundle` — sign, verify and attest the set of files an agent reads.
|
|
5
|
+
*
|
|
6
|
+
* agentmd bundle keygen --out ~/keys/agentmd.key [--kid acme-2026]
|
|
7
|
+
* New Ed25519 key pair. The private key goes to --out, which must be
|
|
8
|
+
* outside this repository; the public key is printed for
|
|
9
|
+
* .agentmd/enterprise.json `bundleKeys` (and your CI secret).
|
|
10
|
+
*
|
|
11
|
+
* agentmd bundle sign --key ~/keys/agentmd.key [--kid acme-2026] [--days 90]
|
|
12
|
+
* Hash every instruction file and sign the result into
|
|
13
|
+
* .agentmd/bundle.sig. In CI, pass the key as AGENTMD_BUNDLE_SIGNING_KEY.
|
|
14
|
+
*
|
|
15
|
+
* agentmd bundle verify [--json]
|
|
16
|
+
* Exit 1 if the signature is not from a trusted key, or if any file an
|
|
17
|
+
* agent reads was modified, removed or added since signing.
|
|
18
|
+
*
|
|
19
|
+
* agentmd bundle attest
|
|
20
|
+
* One JSON record: commit, context digest, bundle status. Store it with
|
|
21
|
+
* the build — it is the answer to "what was the agent told when it
|
|
22
|
+
* wrote this?"
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
"use strict";
|
|
26
|
+
|
|
27
|
+
const fs = require("fs");
|
|
28
|
+
const os = require("os");
|
|
29
|
+
const path = require("path");
|
|
30
|
+
const { execFileSync } = require("child_process");
|
|
31
|
+
const pc = require("picocolors");
|
|
32
|
+
const { keygen, signBundle, verifyBundle, trustedKeys, SIG_FILE } = require("../lib/bundle");
|
|
33
|
+
const { cmd } = require("../lib/invocation");
|
|
34
|
+
|
|
35
|
+
const A = cmd();
|
|
36
|
+
|
|
37
|
+
function flag(args, name) {
|
|
38
|
+
const eq = args.find((a) => a.startsWith(`--${name}=`));
|
|
39
|
+
if (eq) return eq.slice(name.length + 3);
|
|
40
|
+
const i = args.indexOf(`--${name}`);
|
|
41
|
+
return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const expandHome = (p) => (p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
|
|
45
|
+
const fail = (msg) => { console.error(pc.red(`\n ${msg}\n`)); process.exitCode = 1; };
|
|
46
|
+
|
|
47
|
+
function doKeygen(cwd, args) {
|
|
48
|
+
const out = flag(args, "out");
|
|
49
|
+
const kid = flag(args, "kid") || `org-${new Date().toISOString().slice(0, 7)}`;
|
|
50
|
+
if (!out) return fail(`Pass --out <file> for the private key, outside this repository. e.g. ${A} bundle keygen --out ~/keys/agentmd-bundle.key`);
|
|
51
|
+
|
|
52
|
+
const target = path.resolve(expandHome(out));
|
|
53
|
+
const project = path.resolve(cwd);
|
|
54
|
+
if (target === project || target.startsWith(project + path.sep)) {
|
|
55
|
+
return fail("Refusing to write a private key inside this repository — it would be one `git add .` from being published. Choose a path outside it, then store the key in your secret manager.");
|
|
56
|
+
}
|
|
57
|
+
if (fs.existsSync(target)) return fail(`${out} already exists. Not overwriting a key.`);
|
|
58
|
+
|
|
59
|
+
const { publicKey, privateKey } = keygen();
|
|
60
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
61
|
+
fs.writeFileSync(target, privateKey + "\n", { mode: 0o600 });
|
|
62
|
+
|
|
63
|
+
const record = { kid, publicKey };
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(` ${pc.green("✓")} Private key written to ${out} ${pc.dim("(mode 600 — move it into your secret manager)")}`);
|
|
66
|
+
console.log("");
|
|
67
|
+
console.log(" Add the public key to .agentmd/enterprise.json:");
|
|
68
|
+
console.log(pc.dim(` "bundleKeys": [${JSON.stringify(record)}]`));
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(" And in CI, pin it from a secret so a pull request cannot swap it:");
|
|
71
|
+
console.log(pc.dim(` AGENTMD_BUNDLE_KEYS='[${JSON.stringify(record)}]'`));
|
|
72
|
+
console.log("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function doSign(cwd, args) {
|
|
76
|
+
const keyFile = flag(args, "key");
|
|
77
|
+
let privateKey = process.env.AGENTMD_BUNDLE_SIGNING_KEY || null;
|
|
78
|
+
if (keyFile) {
|
|
79
|
+
try { privateKey = fs.readFileSync(path.resolve(expandHome(keyFile)), "utf-8").trim(); } catch (e) { return fail(`Cannot read ${keyFile}: ${e.code || e.message}`); }
|
|
80
|
+
}
|
|
81
|
+
if (!privateKey) return fail(`Pass --key <file>, or set AGENTMD_BUNDLE_SIGNING_KEY. No key yet? ${A} bundle keygen --out ~/keys/agentmd-bundle.key`);
|
|
82
|
+
|
|
83
|
+
// Default kid: the only trusted key, when there is exactly one.
|
|
84
|
+
const { keys } = trustedKeys(cwd);
|
|
85
|
+
const kid = flag(args, "kid") || (keys.length === 1 ? keys[0].kid : null);
|
|
86
|
+
if (!kid) return fail("Pass --kid — there is not exactly one key in bundleKeys to default to.");
|
|
87
|
+
const days = flag(args, "days") ? Number(flag(args, "days")) : undefined;
|
|
88
|
+
if (days !== undefined && !(days > 0)) return fail("--days must be a positive number");
|
|
89
|
+
|
|
90
|
+
let result;
|
|
91
|
+
try { result = signBundle(cwd, privateKey, kid, { days }); } catch (e) {
|
|
92
|
+
return fail(`Could not sign: ${/key/i.test(e.message) ? "the private key is not a valid Ed25519 PKCS8 key" : e.message}`);
|
|
93
|
+
}
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log(` ${pc.green("✓")} Signed ${result.files} file${result.files === 1 ? "" : "s"} with \`${kid}\` → ${SIG_FILE}`);
|
|
96
|
+
console.log(pc.dim(` digest ${result.digest.slice(0, 16)}… Commit ${SIG_FILE} with the files it covers.`));
|
|
97
|
+
if (!keys.some((k) => k.kid === kid)) console.log(pc.yellow(` \`${kid}\` is not in bundleKeys yet — verify will fail until it is.`));
|
|
98
|
+
console.log("");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function doVerify(cwd, args) {
|
|
102
|
+
const r = verifyBundle(cwd);
|
|
103
|
+
// Trusting keys from a file the same pull request can edit means a PR can
|
|
104
|
+
// swap in its own key and re-sign. In CI, the anchor must come from outside.
|
|
105
|
+
if (process.env.CI && trustedKeys(cwd).source !== "AGENTMD_BUNDLE_KEYS") {
|
|
106
|
+
console.error(pc.yellow(" warning: verifying against bundleKeys from the repository itself — a pull request can replace the key and re-sign. Set AGENTMD_BUNDLE_KEYS from a CI secret."));
|
|
107
|
+
}
|
|
108
|
+
if (args.includes("--json")) {
|
|
109
|
+
console.log(JSON.stringify(r, null, 2));
|
|
110
|
+
} else {
|
|
111
|
+
console.log("");
|
|
112
|
+
if (r.valid) {
|
|
113
|
+
console.log(` ${pc.green("✓")} Context bundle verified — signed by \`${r.kid}\` on ${r.signedAt.slice(0, 10)}, nothing changed since.`);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(` ${pc.red("✗")} ${r.reason}`);
|
|
116
|
+
for (const f of r.modified) console.log(` ${pc.yellow("modified")} ${f}`);
|
|
117
|
+
for (const f of r.unapproved) console.log(` ${pc.yellow("unapproved")} ${f}`);
|
|
118
|
+
for (const f of r.missing) console.log(` ${pc.yellow("missing")} ${f}`);
|
|
119
|
+
if (r.modified.length + r.unapproved.length + r.missing.length) console.log(pc.dim(`\n Review the changes, then: ${A} bundle sign --key <file>`));
|
|
120
|
+
}
|
|
121
|
+
console.log("");
|
|
122
|
+
}
|
|
123
|
+
if (!r.valid) process.exitCode = 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function doAttest(cwd) {
|
|
127
|
+
let commit = null;
|
|
128
|
+
try { commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch {}
|
|
129
|
+
const r = verifyBundle(cwd);
|
|
130
|
+
console.log(JSON.stringify({
|
|
131
|
+
type: "agentmd-attestation",
|
|
132
|
+
v: 1,
|
|
133
|
+
at: new Date().toISOString(),
|
|
134
|
+
commit,
|
|
135
|
+
contextDigest: r.digest,
|
|
136
|
+
bundle: { valid: r.valid, kid: r.kid || null, signedAt: r.signedAt || null, reason: r.reason || null },
|
|
137
|
+
}, null, 2));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function bundle(args) {
|
|
141
|
+
args = args || [];
|
|
142
|
+
const cwd = process.cwd();
|
|
143
|
+
const sub = args[0];
|
|
144
|
+
if (sub === "keygen") return doKeygen(cwd, args.slice(1));
|
|
145
|
+
if (sub === "sign") return doSign(cwd, args.slice(1));
|
|
146
|
+
if (sub === "verify") return doVerify(cwd, args.slice(1));
|
|
147
|
+
if (sub === "attest") return doAttest(cwd);
|
|
148
|
+
fail(`Usage: ${A} bundle <keygen|sign|verify|attest>`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { bundle };
|
package/src/commands/ci.js
CHANGED
|
@@ -81,6 +81,11 @@ jobs:
|
|
|
81
81
|
key: agentmd-\${{ runner.os }}-\${{ hashFiles('.agentmd/manifest.json') }}
|
|
82
82
|
restore-keys: agentmd-\${{ runner.os }}-
|
|
83
83
|
|
|
84
|
+
# Every file the agent reads, before the agent reads it: leaked keys,
|
|
85
|
+
# prompt injection, stale version pins. Fails the job on an error.
|
|
86
|
+
- name: Lint agent instruction files
|
|
87
|
+
run: npx --yes activate-agentmd lint
|
|
88
|
+
|
|
84
89
|
- name: Review the diff against the installed standards
|
|
85
90
|
run: npx --yes activate-agentmd review --base origin/\${{ github.base_ref }} --fast --fail-on=high --github
|
|
86
91
|
env:
|
|
@@ -183,7 +188,7 @@ function local(args, cwd) {
|
|
|
183
188
|
console.log(pc.dim(` Commit it, open a pull request, and the pattern review runs on the diff.\n`));
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
const WAITLIST_URL = process.env.AGENTMD_PREVIEW_URL || "https://
|
|
191
|
+
const WAITLIST_URL = process.env.AGENTMD_PREVIEW_URL || "https://agentmd.pages.dev/pro#cloud-ci";
|
|
187
192
|
|
|
188
193
|
/**
|
|
189
194
|
* What `--cloud` prints without `--preview`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint.js
|
|
3
|
+
*
|
|
4
|
+
* `agentmd lint` — check every file your coding agent reads for leaked
|
|
5
|
+
* secrets, prompt injection, bloat, dead references and stale version pins.
|
|
6
|
+
*
|
|
7
|
+
* Works on any repository. No manifest, no account, no network: it reads the
|
|
8
|
+
* files and exits non-zero on an error, so it can sit in CI next to the tests.
|
|
9
|
+
*
|
|
10
|
+
* agentmd lint human-readable report; exit 1 on any error
|
|
11
|
+
* agentmd lint --strict warnings fail too
|
|
12
|
+
* agentmd lint --json machine-readable findings
|
|
13
|
+
* agentmd lint --sarif SARIF 2.1.0, for GitHub code scanning
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const pc = require("picocolors");
|
|
19
|
+
const { lintProject } = require("../lib/lint");
|
|
20
|
+
const { cmd } = require("../lib/invocation");
|
|
21
|
+
const pkg = require("../../package.json");
|
|
22
|
+
|
|
23
|
+
const A = cmd();
|
|
24
|
+
const ORDER = { error: 0, warning: 1, notice: 2 };
|
|
25
|
+
const PAINT = { error: pc.red, warning: pc.yellow, notice: pc.dim };
|
|
26
|
+
|
|
27
|
+
/** GitHub code scanning reads this. Levels map onto SARIF's three. */
|
|
28
|
+
function toSarif(findings) {
|
|
29
|
+
const rules = [...new Set(findings.map((f) => f.rule))].map((id) => ({ id, name: id, shortDescription: { text: `Agent.md instruction-file check: ${id}` } }));
|
|
30
|
+
return {
|
|
31
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
32
|
+
version: "2.1.0",
|
|
33
|
+
runs: [{
|
|
34
|
+
tool: { driver: { name: "agentmd", version: pkg.version, informationUri: "https://github.com/Aaditya1273/Agent.md", rules } },
|
|
35
|
+
results: findings.map((f) => ({
|
|
36
|
+
ruleId: f.rule,
|
|
37
|
+
level: f.severity === "notice" ? "note" : f.severity,
|
|
38
|
+
message: { text: f.message },
|
|
39
|
+
locations: [{ physicalLocation: { artifactLocation: { uri: f.file.split("\\").join("/") }, region: { startLine: f.line || 1 } } }],
|
|
40
|
+
})),
|
|
41
|
+
}],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function lint(args) {
|
|
46
|
+
args = args || [];
|
|
47
|
+
const strict = args.includes("--strict");
|
|
48
|
+
const { files, findings } = lintProject(process.cwd());
|
|
49
|
+
findings.sort((a, b) => ORDER[a.severity] - ORDER[b.severity] || a.file.localeCompare(b.file) || a.line - b.line);
|
|
50
|
+
|
|
51
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
52
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
53
|
+
const failed = errors > 0 || (strict && warnings > 0);
|
|
54
|
+
|
|
55
|
+
if (args.includes("--sarif")) {
|
|
56
|
+
console.log(JSON.stringify(toSarif(findings), null, 2));
|
|
57
|
+
} else if (args.includes("--json")) {
|
|
58
|
+
console.log(JSON.stringify({ files, findings, errors, warnings, failed }, null, 2));
|
|
59
|
+
} else {
|
|
60
|
+
console.log("");
|
|
61
|
+
if (files.length === 0) {
|
|
62
|
+
console.log(` ${pc.dim("No agent instruction files here (CLAUDE.md, AGENTS.md, .cursor/rules, .claude/…). Nothing to check.")}`);
|
|
63
|
+
} else if (findings.length === 0) {
|
|
64
|
+
console.log(` ${pc.green("✓")} ${files.length} instruction file${files.length === 1 ? "" : "s"} checked — no problems.`);
|
|
65
|
+
} else {
|
|
66
|
+
for (const f of findings) {
|
|
67
|
+
const sev = PAINT[f.severity](f.severity.toUpperCase().padEnd(7));
|
|
68
|
+
console.log(` ${sev} ${pc.white(f.file)}${pc.dim(":" + f.line)} ${pc.dim(f.rule)}`);
|
|
69
|
+
console.log(` ${f.message}`);
|
|
70
|
+
}
|
|
71
|
+
console.log("");
|
|
72
|
+
const parts = [`${errors} error${errors === 1 ? "" : "s"}`, `${warnings} warning${warnings === 1 ? "" : "s"}`];
|
|
73
|
+
const notices = findings.length - errors - warnings;
|
|
74
|
+
if (notices) parts.push(`${notices} notice${notices === 1 ? "" : "s"}`);
|
|
75
|
+
console.log(` ${parts.join(", ")} across ${files.length} instruction file${files.length === 1 ? "" : "s"}${strict ? pc.dim(" (--strict: warnings fail)") : ""}`);
|
|
76
|
+
if (findings.some((f) => f.rule === "stale-pins")) console.log(pc.dim(` Fix stale pins with '${A} link'.`));
|
|
77
|
+
}
|
|
78
|
+
console.log("");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (failed) process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { lint, toSarif };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pins.js
|
|
3
|
+
*
|
|
4
|
+
* `agentmd pins` — print the breaking changes for the dependency majors this
|
|
5
|
+
* project actually runs, in the form an agent reads.
|
|
6
|
+
*
|
|
7
|
+
* Exists so a skill or a prompt can fetch current facts at the moment they
|
|
8
|
+
* are needed instead of carrying a copy that goes stale: the skill says
|
|
9
|
+
* "before using a pinned library, run `agentmd pins`", and the facts update
|
|
10
|
+
* whenever the package does. Reads manifests and node_modules; no network.
|
|
11
|
+
*
|
|
12
|
+
* agentmd pins markdown, as `link` writes it
|
|
13
|
+
* agentmd pins --json detected pins, with measured effect where it exists
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
const pc = require("picocolors");
|
|
19
|
+
const { detectVersions, renderPins, proofOf } = require("../lib/versions");
|
|
20
|
+
|
|
21
|
+
async function pins(args) {
|
|
22
|
+
args = args || [];
|
|
23
|
+
const found = detectVersions(process.cwd());
|
|
24
|
+
|
|
25
|
+
if (args.includes("--json")) {
|
|
26
|
+
console.log(JSON.stringify(found.map((p) => ({ ...p, proof: proofOf(p.lib, p.version) })), null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!found.length) {
|
|
30
|
+
console.log(pc.dim("No pinned dependency majors detected (package.json, node_modules, pyproject.toml, requirements.txt, go.mod)."));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// The block is printed exactly as `link` writes it. Measurements are
|
|
34
|
+
// evidence for people, not instructions for the agent, so they are shown
|
|
35
|
+
// here and never written into CLAUDE.md.
|
|
36
|
+
console.log(renderPins(found));
|
|
37
|
+
const measured = found.map((p) => ({ p, proof: proofOf(p.lib, p.version) })).filter((x) => x.proof && x.proof.n);
|
|
38
|
+
for (const { p, proof } of measured) {
|
|
39
|
+
console.log(pc.green(`● measured ${p.label} ${p.version}: old API in ${proof.without}/${proof.n} tasks without the pin, ${proof.with}/${proof.n} with it (${proof.model}, ${proof.date})`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { pins };
|
package/src/commands/review.js
CHANGED
|
@@ -28,7 +28,8 @@ const { readManifest, resolvePresetPath } = require("../lib/manifest");
|
|
|
28
28
|
const { cmd } = require("../lib/invocation");
|
|
29
29
|
const analytics = require("../lib/analytics");
|
|
30
30
|
const { track, EVENTS } = require("../lib/telemetry");
|
|
31
|
-
const { runPatterns } = require("../lib/patterns");
|
|
31
|
+
const { PATTERNS, runPatterns } = require("../lib/patterns");
|
|
32
|
+
const { detectVersions, pinPatterns } = require("../lib/versions");
|
|
32
33
|
|
|
33
34
|
const A = cmd();
|
|
34
35
|
const MODEL = process.env.AGENTMD_REVIEW_MODEL || "claude-opus-5";
|
|
@@ -147,7 +148,8 @@ async function review(args) {
|
|
|
147
148
|
const installedIds = new Set(manifest.presets.map((p) => `${p.category}/${p.preset}`));
|
|
148
149
|
|
|
149
150
|
// Fast-path: deterministic patterns, always, before any model.
|
|
150
|
-
|
|
151
|
+
// Plus the version checks for whatever majors this project actually runs.
|
|
152
|
+
const patternFindings = runPatterns(diff, installedIds, PATTERNS.concat(pinPatterns(detectVersions(cwd))));
|
|
151
153
|
|
|
152
154
|
console.log(pc.bold(`\nReviewing ${pc.cyan(changedFiles.length)} changed file${changedFiles.length === 1 ? "" : "s"} against ${pc.cyan(kept.length)} standard${kept.length === 1 ? "" : "s"}`) + pc.dim(fast ? " (patterns only)" : ` (${modelName}${local ? ", local" : ""})`));
|
|
153
155
|
if (dropped.length) console.log(pc.yellow(` ${dropped.length} standard${dropped.length === 1 ? "" : "s"} left out to fit the request: ${dropped.join(", ")}`));
|
package/src/commands/test.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* agentmd test --all --tasks 5 every installed standard, 5 tasks each
|
|
13
13
|
* agentmd test --fail-under 80 exit 1 below 80% (CI)
|
|
14
14
|
* agentmd test --json machine-readable results
|
|
15
|
+
* agentmd test --baseline also run every task WITHOUT the standard,
|
|
16
|
+
* so you see what the standard changed
|
|
15
17
|
*
|
|
16
18
|
* Needs ANTHROPIC_API_KEY (or an `ant auth login` profile). Costs real API
|
|
17
19
|
* calls: roughly N+2 per standard.
|
|
@@ -96,6 +98,9 @@ async function test(args) {
|
|
|
96
98
|
const cwd = process.cwd();
|
|
97
99
|
const all = args.includes("--all");
|
|
98
100
|
const asJson = args.includes("--json");
|
|
101
|
+
// Without a baseline this measures obedience, not effect: a model that
|
|
102
|
+
// already writes the right thing "obeys" a standard it never needed.
|
|
103
|
+
const baseline = args.includes("--baseline");
|
|
99
104
|
const model = flagValue(args, "model") || DEFAULT_MODEL;
|
|
100
105
|
const tasksPer = Math.min(6, Math.max(1, parseInt(flagValue(args, "tasks") || "3", 10) || 3));
|
|
101
106
|
const failUnder = flagValue(args, "fail-under");
|
|
@@ -170,26 +175,37 @@ async function test(args) {
|
|
|
170
175
|
continue;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
// b. Attempts, with the standard exposed exactly as `link` would
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
// b. Attempts, with the standard exposed exactly as `link` would — and,
|
|
179
|
+
// with --baseline, the same tasks with no standard at all.
|
|
180
|
+
const attempt = (withStandard) => pool(tasks, CONCURRENCY, (t) => call(
|
|
181
|
+
withStandard
|
|
182
|
+
? [{ type: "text", text: "You are a coding agent working in this project. Follow the project's engineering standard below. Reply with code and the minimum prose needed." }, standardBlock]
|
|
183
|
+
: [{ type: "text", text: "You are a coding agent working in this project. Reply with code and the minimum prose needed." }],
|
|
176
184
|
t.task, 8000));
|
|
177
185
|
|
|
178
|
-
// c. Judge strictly against the standard.
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// c. Judge strictly against the standard. Both arms get the identical
|
|
187
|
+
// judge, so any difference is the standard's, not the judge's.
|
|
188
|
+
const judge = async (attempts) => {
|
|
189
|
+
const transcript = tasks.map((t, i) => `<task n="${i + 1}" targets="${String(t.targets).replace(/"/g, "'")}">\n${t.task}\n</task>\n<attempt n="${i + 1}">\n${attempts[i]}\n</attempt>`).join("\n\n");
|
|
190
|
+
const judged = parseJSON(await call(
|
|
191
|
+
[{ type: "text", text: "You are a strict reviewer. Judge each attempt only against the rules in the standard — not general code quality, not taste. An attempt passes if it does not violate the standard. Reply with JSON only: {\"results\":[{\"task\":1,\"pass\":true,\"rule\":\"exact rule heading\",\"reason\":\"one sentence\"}]}. Cite the rule broken (or, on a pass, the rule most relevant)." }, standardBlock],
|
|
192
|
+
transcript, 4000));
|
|
193
|
+
return tasks.map((t, i) => {
|
|
194
|
+
const r = Array.isArray(judged?.results) ? judged.results.find((x) => Number(x.task) === i + 1) : null;
|
|
195
|
+
return { task: i + 1, prompt: t.task, targets: t.targets, pass: r ? !!r.pass : false, rule: r?.rule || t.targets, reason: r?.reason || "no verdict from judge" };
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const results = await judge(await attempt(true));
|
|
200
|
+
const without = baseline ? await judge(await attempt(false)) : null;
|
|
201
|
+
report.push({ id: s.id, tasks, results, ...(without ? { without } : {}) });
|
|
188
202
|
|
|
189
203
|
if (!asJson) {
|
|
190
204
|
const passed = results.filter((r) => r.pass).length;
|
|
191
205
|
const ok = passed === results.length;
|
|
192
|
-
|
|
206
|
+
const base = without ? without.filter((r) => r.pass).length : null;
|
|
207
|
+
const effect = without ? pc.dim(` · without the standard ${base}/${without.length} `) + (passed - base > 0 ? pc.green(`+${passed - base}`) : pc.yellow(`${passed - base >= 0 ? "±" : ""}${passed - base}`)) : "";
|
|
208
|
+
console.log(` ${ok ? pc.green("✓") : pc.red("✗")} ${pc.white(s.id.padEnd(24))} ${passed}/${results.length}${effect}`);
|
|
193
209
|
for (const r of results.filter((x) => !x.pass)) {
|
|
194
210
|
console.log(` ${" ".repeat(30)}${pc.dim(`task ${r.task}: ignored "${r.rule}" — ${r.reason}`)}`);
|
|
195
211
|
}
|
|
@@ -200,12 +216,16 @@ async function test(args) {
|
|
|
200
216
|
const passed = report.reduce((n, r) => n + r.results.filter((x) => x.pass).length, 0);
|
|
201
217
|
const pct = total ? Math.round((passed / total) * 100) : 0;
|
|
202
218
|
const cost = usage.input * PRICE.input + usage.output * PRICE.output;
|
|
219
|
+
const basePassed = baseline ? report.reduce((n, r) => n + (r.without || []).filter((x) => x.pass).length, 0) : null;
|
|
220
|
+
const basePct = baseline && total ? Math.round((basePassed / total) * 100) : null;
|
|
203
221
|
|
|
204
222
|
if (asJson) {
|
|
205
|
-
|
|
223
|
+
const effect = baseline ? { without: { passed: basePassed, total, pct: basePct }, deltaPoints: pct - basePct } : {};
|
|
224
|
+
console.log(JSON.stringify({ model, score: { passed, total, pct }, ...effect, standards: report, usage, estimatedCostUsd: +cost.toFixed(4) }, null, 2));
|
|
206
225
|
} else {
|
|
207
226
|
console.log("");
|
|
208
|
-
console.log(pc.bold(`Score: ${passed}/${total} (${pct}%)`));
|
|
227
|
+
console.log(pc.bold(`Score: ${passed}/${total} (${pct}%)`) + (baseline ? pc.dim(` · without the standards ${basePassed}/${total} (${basePct}%) · effect ${pct - basePct >= 0 ? "+" : ""}${pct - basePct} points`) : ""));
|
|
228
|
+
if (baseline && pct - basePct <= 0) console.log(pc.yellow(" The model did as well without these standards. They cost tokens on every prompt and changed nothing measurable here."));
|
|
209
229
|
for (const r of report) {
|
|
210
230
|
const failed = r.results.filter((x) => !x.pass);
|
|
211
231
|
if (!failed.length) continue;
|
|
@@ -223,4 +243,4 @@ async function test(args) {
|
|
|
223
243
|
}
|
|
224
244
|
}
|
|
225
245
|
|
|
226
|
-
module.exports = { test };
|
|
246
|
+
module.exports = { test, pool, parseJSON };
|
package/src/detect.json
CHANGED
|
@@ -437,6 +437,7 @@
|
|
|
437
437
|
]
|
|
438
438
|
}
|
|
439
439
|
},
|
|
440
|
+
"pinProof": {},
|
|
440
441
|
"categoryGlobs": {
|
|
441
442
|
"Database": "**/*.sql,**/*.prisma,**/prisma/**,**/migrations/**,**/db/**,**/models/**,**/*repository*",
|
|
442
443
|
"Frontend": "**/*.tsx,**/*.jsx,**/*.vue,**/*.svelte,**/*.css,**/app/**,**/components/**,**/pages/**",
|
package/src/lib/agents.js
CHANGED
|
@@ -55,67 +55,30 @@ const tokens = (text) => Math.ceil(String(text || "").length / 4);
|
|
|
55
55
|
const ALWAYS_ON_BUDGET = 1500;
|
|
56
56
|
|
|
57
57
|
/** Packages that earn a place in the core first when the budget is tight. */
|
|
58
|
-
const CORE_PRIORITY = ["Security/owasp", "Review/code-review", "AI/agent-rules"];
|
|
59
|
-
const CATEGORY_RANK = ["Security", "AI", "Review", "Backend", "Frontend", "Database", "API", "Testing", "Performance", "DevOps"];
|
|
60
|
-
|
|
61
58
|
const idOf = (p) => `${p.category}/${p.preset}`;
|
|
62
59
|
const posix = (file) => String(file).split(path.sep).join("/");
|
|
63
60
|
|
|
64
|
-
function corePriority(p) {
|
|
65
|
-
const i = CORE_PRIORITY.indexOf(idOf(p));
|
|
66
|
-
if (i !== -1) return i;
|
|
67
|
-
const c = CATEGORY_RANK.indexOf(p.category);
|
|
68
|
-
return 100 + (c === -1 ? CATEGORY_RANK.length : c) * 10;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
61
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (lines.length >= limit) break;
|
|
88
|
-
lines.push("- " + m.replace(/^- \[ \] (Verify: )?/, ""));
|
|
89
|
-
}
|
|
90
|
-
if (lines.length) return lines.slice(0, limit);
|
|
91
|
-
// Older packages carry neither **Never** rules nor a checklist: fall back to
|
|
92
|
-
// their first substantive bullets so they still earn a place in the core.
|
|
93
|
-
return (body.match(/^- .{40,}$/gm) || []).slice(0, Math.min(4, limit)).map((l) => l.replace(/\*\*/g, ""));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Decide what fits in the always-on block. Pins go first (they are the
|
|
98
|
-
* highest-value lines in the file), then package cores in priority order
|
|
99
|
-
* until the budget is spent. Without a `readPreset` there is nothing to
|
|
100
|
-
* extract, and every package is listed as a link — the pre-budget shape.
|
|
62
|
+
* Decide what is always on. The answer is: the version pins, and nothing else.
|
|
63
|
+
*
|
|
64
|
+
* This used to fill the 1,500-token budget with the "core" of each installed
|
|
65
|
+
* standard — its **Never** rules and checklist — in a hand-ranked priority
|
|
66
|
+
* order. Three independent studies in 2026 then measured what always-on
|
|
67
|
+
* generic guidance does to a coding agent: no gain in task success, a cost
|
|
68
|
+
* increase above 20%, and a measurable drop when the instructions are noisy
|
|
69
|
+
* (Gloaguen et al., ETH Zurich, arXiv 2602.11988; Khatri, arXiv 2607.27250;
|
|
70
|
+
* "Why Does CLAUDE.md Keep Growing?", arXiv 2608.11095). Version-specific API
|
|
71
|
+
* facts were the exception, at +10–20 points (LibEvoBench, arXiv 2606.25402).
|
|
72
|
+
*
|
|
73
|
+
* So pins are always on, and every standard is on demand: linked from the
|
|
74
|
+
* block, and written as a path-scoped reference file for the agents that
|
|
75
|
+
* support one, so it loads when a matching file is open instead of on every
|
|
76
|
+
* prompt. The return shape is unchanged so callers that print it still work.
|
|
101
77
|
*/
|
|
102
78
|
function planAlwaysOn(presets, pins = [], readPreset = null, budget = ALWAYS_ON_BUDGET) {
|
|
103
79
|
const { renderPins } = require("./versions");
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
let used = tokens(renderPins(pins)) + 150; // header, section titles and footer
|
|
107
|
-
if (!readPreset) return { core, demoted: presets.slice(), used, budget };
|
|
108
|
-
|
|
109
|
-
for (const p of presets.slice().sort((a, b) => corePriority(a) - corePriority(b))) {
|
|
110
|
-
const content = readPreset(p);
|
|
111
|
-
const lines = content ? coreOf(content) : [];
|
|
112
|
-
if (!lines.length) { demoted.push(p); continue; }
|
|
113
|
-
const cost = tokens(lines.join("\n")) + tokens(idOf(p)) + 8;
|
|
114
|
-
if (used + cost > budget) { demoted.push(p); continue; }
|
|
115
|
-
used += cost;
|
|
116
|
-
core.push({ id: idOf(p), file: posix(p.file), lines });
|
|
117
|
-
}
|
|
118
|
-
return { core, demoted, used, budget };
|
|
80
|
+
const used = tokens(renderPins(pins)) + 150; // header, section titles and footer
|
|
81
|
+
return { core: [], demoted: presets.slice(), used, budget };
|
|
119
82
|
}
|
|
120
83
|
|
|
121
84
|
/**
|
|
@@ -298,4 +261,4 @@ function scopedRules(presets, readPreset) {
|
|
|
298
261
|
return referenceRules(presets, readPreset, "cursor");
|
|
299
262
|
}
|
|
300
263
|
|
|
301
|
-
module.exports = { TARGETS, CATEGORY_GLOBS, REFERENCE_DIRS, ALWAYS_ON_BUDGET, applyToFile, renderBody, planAlwaysOn,
|
|
264
|
+
module.exports = { TARGETS, CATEGORY_GLOBS, REFERENCE_DIRS, ALWAYS_ON_BUDGET, applyToFile, renderBody, planAlwaysOn, referenceRules, scopedRules, tokens, START, END };
|
package/src/lib/analytics.js
CHANGED
|
@@ -46,10 +46,16 @@ function read(cwd = process.cwd()) {
|
|
|
46
46
|
function record(cwd, { findings = [], standards = [], mode = "patterns", base = null } = {}) {
|
|
47
47
|
try {
|
|
48
48
|
const ledger = read(cwd);
|
|
49
|
+
// Which instructions were in force for this run — one hash over every
|
|
50
|
+
// file an agent reads. Lets a finding be traced to the exact context it
|
|
51
|
+
// was checked against, which is what an audit of agent-written code asks.
|
|
52
|
+
let contextHash = null;
|
|
53
|
+
try { contextHash = require("./bundle").contextDigest(cwd); } catch {}
|
|
49
54
|
ledger.runs.push({
|
|
50
55
|
at: new Date().toISOString(),
|
|
51
56
|
mode,
|
|
52
57
|
base,
|
|
58
|
+
contextHash,
|
|
53
59
|
standards: standards.length,
|
|
54
60
|
findings: findings.map((f) => ({
|
|
55
61
|
severity: String(f.severity || "low"),
|