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 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://agent-dot-md.vercel.app/pro"))}
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: "validate",
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.4.0",
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 };
@@ -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://agent-dot-md.vercel.app/pro#cloud-ci";
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 };
@@ -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
- const patternFindings = runPatterns(diff, installedIds);
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(", ")}`));
@@ -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
- const attempts = await pool(tasks, CONCURRENCY, (t) => call(
175
- [{ 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],
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
- 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");
180
- const judged = parseJSON(await call(
181
- [{ 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],
182
- transcript, 4000));
183
- const results = tasks.map((t, i) => {
184
- const r = Array.isArray(judged?.results) ? judged.results.find((x) => Number(x.task) === i + 1) : null;
185
- 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" };
186
- });
187
- report.push({ id: s.id, tasks, results });
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
- console.log(` ${ok ? pc.green("✓") : pc.red("✗")} ${pc.white(s.id.padEnd(24))} ${passed}/${results.length}`);
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
- console.log(JSON.stringify({ model, score: { passed, total, pct }, standards: report, usage, estimatedCostUsd: +cost.toFixed(4) }, null, 2));
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
- * A package's core: its hard rules and its checklist — the lines an agent
73
- * must hold on every prompt. Derived from the standard itself rather than
74
- * hand-written, so every package has one today; a canonical file can tighten
75
- * it by improving its own **Never** rules and checklist.
76
- */
77
- function coreOf(content, limit = 6) {
78
- const body = String(content).replace(/^---[\s\S]*?---\s*/, "");
79
- const lines = [];
80
- for (const para of body.split(/\n\s*\n/)) {
81
- const t = para.trim();
82
- if (/^\*\*Never\*\*|^- \*\*Never/.test(t)) {
83
- lines.push("- " + t.replace(/^[-*\s]*/, "").replace(/\*\*/g, "").replace(/\s*\n\s*/g, " "));
84
- }
85
- }
86
- for (const m of body.match(/^- \[ \] .+$/gm) || []) {
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 core = [];
105
- const demoted = [];
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, coreOf, referenceRules, scopedRules, tokens, START, END };
264
+ module.exports = { TARGETS, CATEGORY_GLOBS, REFERENCE_DIRS, ALWAYS_ON_BUDGET, applyToFile, renderBody, planAlwaysOn, referenceRules, scopedRules, tokens, START, END };
@@ -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"),