efaimo 0.1.0 → 0.1.2
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/CHANGELOG.md +160 -4
- package/README.md +89 -31
- package/SKILL.md +13 -1
- package/dist/check/check.js +14 -2
- package/dist/cli.js +95 -9
- package/dist/clients/introspect.js +10 -2
- package/dist/clients/rawprobe.d.ts +21 -5
- package/dist/clients/rawprobe.js +101 -43
- package/dist/clients/stateless.js +17 -14
- package/dist/core/engine.js +20 -2
- package/dist/core/grade.js +7 -1
- package/dist/core/types.d.ts +22 -3
- package/dist/reporters/markdown.js +16 -4
- package/dist/reporters/pretty.js +38 -25
- package/dist/rules/injection.d.ts +1 -2
- package/dist/rules/injection.js +7 -11
- package/dist/rules/mcp/index.d.ts +14 -0
- package/dist/rules/mcp/index.js +89 -25
- package/dist/rules/skill/index.js +5 -3
- package/dist/skills/parse.js +39 -3
- package/dist/targets/clientConfigs.d.ts +7 -0
- package/dist/targets/clientConfigs.js +4 -1
- package/dist/targets/repoScan.js +0 -0
- package/dist/testing/anthropicRunner.js +2 -0
- package/dist/testing/harness.d.ts +22 -0
- package/dist/testing/harness.js +98 -13
- package/dist/testing/openaiRunner.js +15 -1
- package/dist/testing/stats.d.ts +30 -0
- package/dist/testing/stats.js +89 -0
- package/dist/util/dotenv.d.ts +9 -1
- package/dist/util/dotenv.js +11 -1
- package/dist/util/safeText.d.ts +8 -0
- package/dist/util/safeText.js +48 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/weigh/weigh.js +3 -1
- package/dist/weigh/window.d.ts +26 -0
- package/dist/weigh/window.js +44 -0
- package/docs/METHODOLOGY.md +70 -0
- package/docs/RULES.md +31 -25
- package/examples/contoso-crm-import/SKILL.md +21 -0
- package/examples/csv-cleanup/SKILL.md +17 -0
- package/examples/scenario.crm.yaml +22 -0
- package/examples/scenario.example.yaml +17 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,163 @@ All notable changes to efaimo are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.1.2] - 2026-08-03
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The 0.1.2 code backlog: edge-case fixes surfaced by the five-agent review, none
|
|
10
|
+
of which changes a published number or the documented command surface, plus the
|
|
11
|
+
security and CI hardening already staged.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `check --mcp <directory>` with no source files and no MCP SDK dependency now
|
|
16
|
+
fails instead of returning a clean A(100): an empty scan is a failure, never a
|
|
17
|
+
pass.
|
|
18
|
+
- S101: a skill with no frontmatter reports its missing `name` and `description`
|
|
19
|
+
as errors, so a total absence no longer grades higher than a broken one.
|
|
20
|
+
- E105: a server that exits on a bare stateless `tools/list` is flagged. The
|
|
21
|
+
crash outcome was unhandled, so a crashing server looked more migrated than
|
|
22
|
+
one that answers with a not-initialized error.
|
|
23
|
+
- E124: the "N of M parameters lack descriptions" summary is no longer dropped
|
|
24
|
+
by the display cap on schemas with five or more oversized enums.
|
|
25
|
+
- S106: the escape check is lexical (the resolved path), not a `..` substring,
|
|
26
|
+
so a file merely named `notes..md` is not flagged as escaping.
|
|
27
|
+
- A Poetry `mcp = "^2.0"` constraint in `pyproject.toml` is read as the 2.x
|
|
28
|
+
line, not legacy; the version regex had captured the `=` assignment.
|
|
29
|
+
- `weigh --diff` with a skill baseline fails with a clear message instead of a
|
|
30
|
+
raw TypeError; a server baseline is required.
|
|
31
|
+
- `--max-tokens` and `--allow-increase` reject non-positive / negative values
|
|
32
|
+
instead of turning the gate into an always-fail with a nonsense message.
|
|
33
|
+
- `efaimo test --live` against an OpenAI o-series reasoning model no longer
|
|
34
|
+
sends an unsupported `temperature` (a 400) and gives it token headroom.
|
|
35
|
+
- The live test harness retries a transient 429 or 5xx with backoff instead of
|
|
36
|
+
discarding every already-paid trial on the first blip.
|
|
37
|
+
- The test report prints a clean note instead of "95% interval NaN to NaN" when
|
|
38
|
+
an arm produces no scoreable trial.
|
|
39
|
+
- A dead stdio target fails in about one `--timeout`, not two: the legacy
|
|
40
|
+
handshake and the stateless fallback now share the budget.
|
|
41
|
+
- Internal: dropped a dead exported constant and the unused per-pattern
|
|
42
|
+
`severity` on the injection heuristics (every hit is info + ungraded by
|
|
43
|
+
design).
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
- The lockfile was re-resolved so the dev/CI tree carries
|
|
48
|
+
`@hono/node-server` 2.0.12 instead of the 1.19.x line that
|
|
49
|
+
GHSA-frvp-7c67-39w9 covers (moderate, path traversal in `serve-static`, in
|
|
50
|
+
the half of the MCP SDK this CLI never executes). A fresh `npx efaimo`
|
|
51
|
+
install already resolved the patched line; this aligns the committed tree
|
|
52
|
+
with what users actually get, and `pnpm audit --prod` is clean again.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- `@modelcontextprotocol/sdk` floor moved within-range from ^1.29.0 to ^1.30.0
|
|
57
|
+
(what a fresh install resolves anyway). The full suite (136 tests) is green.
|
|
58
|
+
- CI: the reference-server smoke is pinned to
|
|
59
|
+
`@modelcontextprotocol/server-everything@2026.7.4`. Per-commit CI should be
|
|
60
|
+
deterministic; news about the reference server's latest belongs to a
|
|
61
|
+
scheduled run, not to a push.
|
|
62
|
+
- CI: `spec-drift.yml` and the example workflow now pin actions by commit SHA
|
|
63
|
+
like the other workflows, and the example declares least-privilege
|
|
64
|
+
`permissions`.
|
|
65
|
+
|
|
66
|
+
## [0.1.1] - 2026-08-02
|
|
67
|
+
|
|
68
|
+
Everything below was on `main` and unpublished. That gap was itself the
|
|
69
|
+
problem: `efaimo@0.1.0` still tells every user "the spec finalizes
|
|
70
|
+
2026-07-28" (a future tense about a date that has passed), the README
|
|
71
|
+
documents `--strict-readiness` and `--window`, and neither flag exists in
|
|
72
|
+
0.1.0. Cutting this release is what makes the documentation true.
|
|
73
|
+
|
|
74
|
+
The MCP specification published on 2026-07-28. The readiness rules here had
|
|
75
|
+
been written against the Release Candidate locked 2026-05-21, and the two are
|
|
76
|
+
not the same document: `DiscoverResult.serverInfo` was deleted,
|
|
77
|
+
`DiscoverResult` became cacheable, and three error codes were renumbered. The
|
|
78
|
+
entries below that name the published spec are the repairs, plus the tooling
|
|
79
|
+
that notices the next revision by itself.
|
|
80
|
+
|
|
81
|
+
### Security
|
|
82
|
+
|
|
83
|
+
- `--client` now prints the actual command of every server it is about to
|
|
84
|
+
spawn, and warns by name when a config came from the CURRENT DIRECTORY
|
|
85
|
+
rather than your user config. It reads `.mcp.json`, `.cursor/mcp.json` and
|
|
86
|
+
`.vscode/mcp.json` from the working directory and executes what it finds, so
|
|
87
|
+
running the audit inside a repository you cloned was arbitrary code
|
|
88
|
+
execution, announced only by a friendly config key.
|
|
89
|
+
- Terminal control characters are stripped from anything the audited target
|
|
90
|
+
supplied, at the render boundary of the pretty and markdown reporters. A
|
|
91
|
+
hostile server could put ESC[1A ESC[2K in a tool name and rewrite the grade
|
|
92
|
+
line printed above its own finding, so the reader saw "grade A (100), 0
|
|
93
|
+
errors" while the contradiction was erased. `--no-color` did not help; that
|
|
94
|
+
only suppresses colors efaimo adds.
|
|
95
|
+
- `.env` loading is restricted to `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`.
|
|
96
|
+
It previously set ANY key from a working-directory `.env`, so a cloned
|
|
97
|
+
repository could set `NODE_TLS_REJECT_UNAUTHORIZED=0` and silently disable
|
|
98
|
+
certificate verification for later HTTPS calls, including ones carrying your
|
|
99
|
+
key.
|
|
100
|
+
- Skill references that resolve outside the skill directory are no longer read
|
|
101
|
+
by `weigh`. S106 already warned about them, but only after the read.
|
|
102
|
+
- `SKILL.md` is read through a capped read (2MB) on the same guarded path that
|
|
103
|
+
caps referenced files at 512KB, instead of an uncapped read.
|
|
104
|
+
- The published composite Action no longer interpolates its inputs into a bash
|
|
105
|
+
`run:` body, which was script injection in the consumer's runner.
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- `check --strict-readiness`: exit non-zero when the 2026-07-28 migration diff
|
|
110
|
+
is not clean. Readiness still never touches the grade or the badge, and the
|
|
111
|
+
default exit code is unchanged. It exists because a team that had finished
|
|
112
|
+
migrating had no way to stay migrated: `--strict` covers quality only, so
|
|
113
|
+
nothing in CI could catch a regression back onto the legacy handshake.
|
|
114
|
+
- `--window <tokens>` on `weigh` and `check`: report the context-window share
|
|
115
|
+
against a window you name instead of the 1,000,000-token default. Absolute
|
|
116
|
+
token counts do not change; only the denominator does.
|
|
117
|
+
- `npm run spec:drift`, plus a daily `spec-drift` workflow: fails when an MCP
|
|
118
|
+
revision newer than the one efaimo targets publishes, or when the targeted
|
|
119
|
+
revision changes under its own tag. The 2026-07-28 publication went unnoticed
|
|
120
|
+
in this repo for four days because nothing watched the spec itself.
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
|
|
124
|
+
- `check --conformance` pins `@modelcontextprotocol/conformance@0.2.0-alpha.10`
|
|
125
|
+
rather than the floating `alpha` tag, which had moved off `alpha.9` on
|
|
126
|
+
2026-07-27. A dist-tag is not a pin, and the old one meant reporting "the
|
|
127
|
+
official conformance suite" for a build nobody here had run.
|
|
128
|
+
- Readiness output no longer claims the spec "finalizes 2026-07-28". It has
|
|
129
|
+
published; the diff still prints ungraded.
|
|
130
|
+
- Rule detail lines name the revision by its date instead of calling it "the
|
|
131
|
+
RC", which it no longer is.
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- `efaimo test` reports a two-sided Fisher exact p and a 95% interval, and
|
|
136
|
+
`helps`/`hurts` require p < 0.05. The old rule was a +-15 point threshold at
|
|
137
|
+
a default of 5 trials per arm, where 5/5 against 4/5 is +20 points and
|
|
138
|
+
p = 1.0000. Judge now runs at temperature 0; an unparseable judge verdict is
|
|
139
|
+
excluded rather than scored FAIL; default trials 5 -> 20.
|
|
140
|
+
- E123 fires. The matcher was `(delete|...)` and `_` is a word character,
|
|
141
|
+
so `delete_file`, `deleteFile` and `drop_table` never matched: the rule was
|
|
142
|
+
dead for every conventionally named tool.
|
|
143
|
+
- A rule that throws is reported as an ungraded E000 instead of being silently
|
|
144
|
+
swallowed, which used to RAISE the grade.
|
|
145
|
+
- `weigh`'s heaviest-tools column derives its width from the rows printed, so
|
|
146
|
+
a 30-character tool name no longer shifts the number column.
|
|
147
|
+
- The median in `scripts/skills-index.mjs` is the true median.
|
|
148
|
+
- `check --mcp` could hang forever on a stdio server. The readiness probes
|
|
149
|
+
cleaned up their spawned servers only on the happy path, so a probe that threw
|
|
150
|
+
left the child's stdin open; the child never saw EOF, never exited, and its
|
|
151
|
+
stdio handles kept the CLI's event loop alive. The report printed and then the
|
|
152
|
+
process simply never returned. Cleanup now runs in a `finally`, matching the
|
|
153
|
+
stateless introspection path next door, which already did.
|
|
154
|
+
- `check --mcp` and `weigh` lost server identity on servers that had finished
|
|
155
|
+
migrating. The published spec deleted `DiscoverResult.serverInfo` and moved
|
|
156
|
+
identity into `_meta["io.modelcontextprotocol/serverInfo"]`, and efaimo read
|
|
157
|
+
only the Release Candidate's field. Both shapes are read now, newest first.
|
|
158
|
+
- E118 measured cache fields on `tools/list` only, while its own detail line
|
|
159
|
+
already listed `server/discover`. The published spec made `DiscoverResult`
|
|
160
|
+
extend `CacheableResult`, so that surface is required now, is measured now,
|
|
161
|
+
and the message names whichever surface failed.
|
|
162
|
+
|
|
163
|
+
## [0.1.0] - 2026-07-18
|
|
10
164
|
|
|
11
165
|
First release.
|
|
12
166
|
|
|
@@ -19,7 +173,7 @@ First release.
|
|
|
19
173
|
broken or auth-gated server is skipped with a reason, not fatal.
|
|
20
174
|
`--out`/`--diff`/`--max-tokens`/`--allow-increase` for CI budget gates,
|
|
21
175
|
`--badge` for a shields endpoint.
|
|
22
|
-
- `efaimo check --mcp`: a quality grade (E121-E130: descriptions, schemas,
|
|
176
|
+
- `efaimo check --mcp`: a quality grade (E121-E128, E130: descriptions, schemas,
|
|
23
177
|
annotations, cost) plus a separate, ungraded 2026-07-28 migration diff
|
|
24
178
|
(E101-E118: what the stateless spec breaks and how to fix it, each item naming
|
|
25
179
|
its SEP). Speaks both the legacy handshake and bare stateless requests, so
|
|
@@ -43,5 +197,7 @@ First release.
|
|
|
43
197
|
Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
|
|
44
198
|
and integration guide (`docs/INTEGRATIONS.md`).
|
|
45
199
|
|
|
46
|
-
[
|
|
200
|
+
[unreleased]: https://github.com/efaimo-ai/efaimo/compare/v0.1.2...HEAD
|
|
201
|
+
[0.1.2]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.2
|
|
202
|
+
[0.1.1]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.1
|
|
47
203
|
[0.1.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ fires at all. Good tools exist for single slices of that problem; efaimo's job i
|
|
|
21
21
|
the whole audit in one command, for both halves of what an agent loads. Lint a
|
|
22
22
|
skill, weigh a server's tool definitions, get a migration diff for the 2026-07-28
|
|
23
23
|
stateless MCP spec, and measure whether a skill actually improves task completion
|
|
24
|
-
([SkillsBench](https://www.skillsbench.ai) found
|
|
25
|
-
|
|
24
|
+
([SkillsBench](https://www.skillsbench.ai) found curated skills made agents
|
|
25
|
+
worse on 16 of 84 tasks; linting alone cannot catch that).
|
|
26
26
|
|
|
27
27
|
## Agent Skills
|
|
28
28
|
|
|
@@ -35,11 +35,11 @@ Point it at one skill or a whole folder. It validates each skill against the
|
|
|
35
35
|
|
|
36
36
|
```text
|
|
37
37
|
check skill claude-api
|
|
38
|
-
grade C (
|
|
38
|
+
grade C (73) 1 error 2 warnings 4 info
|
|
39
39
|
|
|
40
40
|
x S101 description is 1068 chars (spec max 1024)
|
|
41
41
|
! S104 instructions are ~18.4k tokens (spec recommends staying under 5k)
|
|
42
|
-
fix: move detail into references/ files loaded on demand
|
|
42
|
+
fix: move detail into references/ files loaded on demand (progressive disclosure)
|
|
43
43
|
! S104 SKILL.md is 570 lines (spec recommends under 500)
|
|
44
44
|
i S104 metadata level is ~294 tokens; the spec targets ~100 and this loads at
|
|
45
45
|
startup for every installed skill
|
|
@@ -53,8 +53,8 @@ hygiene. Run it on a folder and every skill gets its own grade, not one aggregat
|
|
|
53
53
|
**We graded 36 public Agent Skills**: every skill in `anthropics/skills`,
|
|
54
54
|
`anthropics/claude-cookbooks`, and `obra/superpowers`, at pinned commits. 97%
|
|
55
55
|
score an A, but even this curated set has real issues: Anthropic's own
|
|
56
|
-
`claude-api` scores a C with an over-limit description and ~18k-token
|
|
57
|
-
instructions, and the median skill's instructions run ~1,
|
|
56
|
+
`claude-api` scores a C (73) with an over-limit description and ~18k-token
|
|
57
|
+
instructions, and the median skill's instructions run ~1,632 tokens loaded on
|
|
58
58
|
every trigger. Full report, the corpus manifest, and the two commands that
|
|
59
59
|
reproduce it: [the Skills Quality Index](./research/skills-index/REPORT.md).
|
|
60
60
|
|
|
@@ -103,9 +103,19 @@ test contoso-crm-import helps on an unknowable format
|
|
|
103
103
|
</details>
|
|
104
104
|
|
|
105
105
|
The first is pure context overhead, the second earns its tokens, and only the
|
|
106
|
-
trial data tells them apart.
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
trial data tells them apart.
|
|
107
|
+
|
|
108
|
+
The verdict is a significance test, not a threshold on the gap. Every run
|
|
109
|
+
reports a two-sided Fisher exact `p` and a 95% interval on the delta, and
|
|
110
|
+
`helps` / `hurts` require `p < 0.05`; anything else is `no measurable effect`
|
|
111
|
+
no matter how large the gap looks. That matters more than it sounds: at the
|
|
112
|
+
old default of 5 trials per arm, 5/5 against 4/5 is +20 points and a `p` of
|
|
113
|
+
exactly 1.0000, and the previous rule called it `helps`. The `+100` run above
|
|
114
|
+
is `p = 0.0002`; a `+25` run at 8 trials per arm is `p = 0.47` and now says so.
|
|
115
|
+
The default is 20 trials per arm, because below about that a partial result
|
|
116
|
+
cannot reach significance at all.
|
|
117
|
+
|
|
118
|
+
It is opt-in because a live run spends tokens on your key. See
|
|
109
119
|
[examples/scenario.example.yaml](./examples/scenario.example.yaml) and
|
|
110
120
|
[examples/scenario.crm.yaml](./examples/scenario.crm.yaml).
|
|
111
121
|
|
|
@@ -128,18 +138,21 @@ stateless protocol, so a 2026-07-28 server audits fine) and reports two things,
|
|
|
128
138
|
separately. First, a **quality grade** for what models actually experience:
|
|
129
139
|
descriptions, schemas, annotations, tool count, token cost. Second, an ungraded
|
|
130
140
|
**migration diff** for the 2026-07-28 stateless spec (which removes
|
|
131
|
-
`initialize
|
|
141
|
+
`initialize` and sessions, deprecates Sampling, Roots, and Logging, and requires
|
|
132
142
|
`server/discover`, `resultType`, and cache fields): exactly what will break and
|
|
133
143
|
how to fix it, each rule naming the SEP it came from. Readiness never drags the
|
|
134
|
-
grade
|
|
135
|
-
|
|
144
|
+
grade, because the two answer different questions: the grade is what a model
|
|
145
|
+
experiences from this tool surface, readiness is whether a 2026-07-28 client can
|
|
146
|
+
talk to this server. Combining them scored the reference server a C for a tool
|
|
147
|
+
surface that is an A. Pass `--strict-readiness` when you want CI to fail on
|
|
148
|
+
the diff. Full list: [docs/RULES.md](./docs/RULES.md).
|
|
136
149
|
|
|
137
150
|
<p align="center">
|
|
138
151
|
<img src="assets/check.png" alt="efaimo check example output: quality grade A (95) plus a four-item 2026-07-28 migration diff for the reference server" width="800">
|
|
139
152
|
</p>
|
|
140
153
|
|
|
141
|
-
*(That is the official reference server: solid quality, four things to
|
|
142
|
-
|
|
154
|
+
*(That is the official reference server: solid quality, four things left to
|
|
155
|
+
migrate.)*
|
|
143
156
|
|
|
144
157
|
<details>
|
|
145
158
|
<summary>The same output as copyable text</summary>
|
|
@@ -151,8 +164,7 @@ grade A (95) quality: 0 errors 1 warning 0 info
|
|
|
151
164
|
! E122 tool "echo": description misses 3/4 quality axes (length 40..600;
|
|
152
165
|
says when to use it; mentions the result)
|
|
153
166
|
|
|
154
|
-
2026-07-28 readiness 4 items to migrate (a migration diff, not graded
|
|
155
|
-
spec finalizes 2026-07-28)
|
|
167
|
+
2026-07-28 readiness 4 items to migrate (a migration diff, not graded)
|
|
156
168
|
! E104 server declares the logging capability; MCP Logging is deprecated in
|
|
157
169
|
2026-07-28 (SEP-2577) and logging/setLevel is removed
|
|
158
170
|
! E106 server/discover is not implemented (-32601 Method not found)
|
|
@@ -192,19 +204,56 @@ your context window.
|
|
|
192
204
|
Focused tools already own single slices of this space, and some are very good.
|
|
193
205
|
efaimo is the one tool that covers the whole audit surface. As of mid-2026:
|
|
194
206
|
|
|
195
|
-
| | efaimo | [skill-validator](https://github.com/agent-ecosystem/skill-validator) | [upskill](https://github.com/huggingface/upskill) | [mcp-spec-check](https://github.com/Roee-Tsur/mcp-spec-check) | [conformance](https://github.com/modelcontextprotocol/conformance) (official) |
|
|
196
|
-
|
|
197
|
-
| Agent Skills linting | yes | yes | no | no | no |
|
|
198
|
-
| Skill token cost | yes (3-level split) | yes | no | no | no |
|
|
199
|
-
| Skill outcome testing | with/without A/B trials (experimental) | static LLM scoring | yes (generate + eval, code agents) | no | no |
|
|
200
|
-
| MCP tool-definition cost | yes (3 serializations, `--anthropic` exact) | no | no | no | no |
|
|
201
|
-
| MCP quality rules | yes | no | no | no | no |
|
|
202
|
-
| 2026-07-28 readiness | migration diff: what breaks and how to fix | no | no | yes/no verdict | official test suite |
|
|
203
|
-
| CI budget gate + badge | yes | no | no | no | no |
|
|
207
|
+
| | efaimo | [skill-validator](https://github.com/agent-ecosystem/skill-validator) | [skillgrade](https://github.com/mgechev/skillgrade) | [upskill](https://github.com/huggingface/upskill) | [mcp-spec-check](https://github.com/Roee-Tsur/mcp-spec-check) | [conformance](https://github.com/modelcontextprotocol/conformance) (official) |
|
|
208
|
+
|---|---|---|---|---|---|---|
|
|
209
|
+
| Agent Skills linting | yes | yes | no | no | no | no |
|
|
210
|
+
| Skill token cost | yes (3-level split) | yes | no | no | no | no |
|
|
211
|
+
| Skill outcome testing | with/without A/B trials (experimental) | static LLM scoring | **yes, and more thoroughly**: Docker-isolated runs, script + LLM-rubric graders, weighted scores, CI pass-rate gates | yes (generate + eval, code agents) | no | no |
|
|
212
|
+
| MCP tool-definition cost | yes (3 serializations, `--anthropic` exact) | no | no | no | no | no |
|
|
213
|
+
| MCP quality rules | yes | no | no | no | no | no |
|
|
214
|
+
| 2026-07-28 readiness | migration diff: what breaks and how to fix | no | no | no | yes/no verdict | official test suite |
|
|
215
|
+
| CI budget gate + badge | yes (token budget) | no | pass-rate threshold | no | no | no |
|
|
216
|
+
|
|
217
|
+
Read that table honestly: on skill outcome testing, `skillgrade` is the more
|
|
218
|
+
serious tool and we would use it over our own `efaimo test` for anything
|
|
219
|
+
depending on the result. Ours is one experimental subcommand in an audit CLI;
|
|
220
|
+
theirs is a testing framework. efaimo's claim is the row count, not any single
|
|
221
|
+
row.
|
|
222
|
+
|
|
223
|
+
[SkillsBench](https://github.com/benchflow-ai/skillsbench), whose paper this
|
|
224
|
+
README cites, is not in the table because it is a benchmark rather than a tool
|
|
225
|
+
you point at your own work: a fixed suite of tasks with packaged environments
|
|
226
|
+
and verifiers, extensible by contributing tasks upstream. Useful for the
|
|
227
|
+
question "do skills help in general", which is why we cite it, and not for
|
|
228
|
+
"does mine help", which is what `efaimo test` and `skillgrade` answer.
|
|
204
229
|
|
|
205
230
|
efaimo runs the official conformance suite for you (`check --conformance` on
|
|
206
231
|
http targets) and complements security scanners such as Snyk agent-scan and the
|
|
207
|
-
skills installer (`npx skills`) rather than replacing them.
|
|
232
|
+
skills installer (`npx skills`) rather than replacing them. If your server is
|
|
233
|
+
built on the TypeScript SDK, efaimo tells you what breaks and why, and
|
|
234
|
+
[`@modelcontextprotocol/codemod`](https://www.npmjs.com/package/@modelcontextprotocol/codemod)
|
|
235
|
+
rewrites the SDK v1 to v2 call sites for you. Diff, codemod, re-check.
|
|
236
|
+
|
|
237
|
+
### "Doesn't tool search already solve this?"
|
|
238
|
+
|
|
239
|
+
Partly, and that is the point. Anthropic ships a
|
|
240
|
+
[tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool):
|
|
241
|
+
mark tools `defer_loading: true` and the model discovers schemas on demand
|
|
242
|
+
instead of carrying all of them, with discovered schemas appended so the prompt
|
|
243
|
+
cache survives. Where it applies, it is a better fix than anything a linter can
|
|
244
|
+
tell you, and efaimo's own rules point at it (E127 and E128 suggest deferred
|
|
245
|
+
loading as a remedy).
|
|
246
|
+
|
|
247
|
+
It does not make measurement unnecessary, for three reasons. It is opt-in per
|
|
248
|
+
tool, so somebody has to decide which tools are worth deferring, and that
|
|
249
|
+
decision needs the per-tool numbers. It is a client-side feature, so it says
|
|
250
|
+
nothing about what your server publishes to hosts that do not implement it.
|
|
251
|
+
And a mitigation you cannot measure is a mitigation you are trusting on faith:
|
|
252
|
+
`weigh` reports the definitions your server actually ships, which is the input
|
|
253
|
+
every mitigation operates on.
|
|
254
|
+
|
|
255
|
+
If deferred loading covers your whole setup, you may not need this tool. Run
|
|
256
|
+
`weigh` once and find out; that answer is worth more than our opinion.
|
|
208
257
|
|
|
209
258
|
## Install
|
|
210
259
|
|
|
@@ -222,6 +271,9 @@ Or add it to a project with `npm i -D efaimo`, or the GitHub Action:
|
|
|
222
271
|
command: check --skill ./skills --strict
|
|
223
272
|
```
|
|
224
273
|
|
|
274
|
+
The Action is listed on the
|
|
275
|
+
[GitHub Marketplace](https://github.com/marketplace/actions/efaimo).
|
|
276
|
+
|
|
225
277
|
Gate a pull request on context-window growth, too:
|
|
226
278
|
|
|
227
279
|
```bash
|
|
@@ -243,11 +295,16 @@ More recipes (pre-commit, GitLab, editor audit, programmatic use):
|
|
|
243
295
|
|---|---|
|
|
244
296
|
| **S101 to S106** | skills: frontmatter and trigger quality, trigger collisions, context budget, reference integrity, injection hygiene |
|
|
245
297
|
| **E101 to E118** | MCP 2026-07-28 readiness: deprecated primitives, statelessness, `server/discover`, `resultType`, cache fields, transport |
|
|
246
|
-
| **E121 to E130** | MCP quality: description quality, annotations, schema hygiene, tool-count and token-cost budgets |
|
|
298
|
+
| **E121 to E128, E130** | MCP quality: description quality, annotations, schema hygiene, tool-count and token-cost budgets |
|
|
299
|
+
|
|
300
|
+
`E000` is not a finding about your server: it means one of efaimo's own rules
|
|
301
|
+
threw and was skipped, so the report is incomplete. It is never scored.
|
|
247
302
|
|
|
248
303
|
Quality (E12x-E13x) and skill (S) findings set the letter grade; readiness
|
|
249
|
-
findings (E101-E118) are reported as an ungraded migration diff
|
|
250
|
-
|
|
304
|
+
findings (E101-E118) are reported as an ungraded migration diff, which
|
|
305
|
+
`--strict-readiness` can turn into a non-zero exit
|
|
306
|
+
without touching the grade.
|
|
307
|
+
Every finding carries a stable id you can suppress or link. See
|
|
251
308
|
[docs/RULES.md](./docs/RULES.md).
|
|
252
309
|
|
|
253
310
|
## Roadmap
|
|
@@ -255,8 +312,9 @@ ratifies. Every finding carries a stable id you can suppress or link. See
|
|
|
255
312
|
- Harden `efaimo test`: a separately chosen judge model, confidence intervals on
|
|
256
313
|
the delta, multi-turn tool-use trials, and judge calibration, so the
|
|
257
314
|
experimental harness earns unqualified trust.
|
|
258
|
-
-
|
|
259
|
-
|
|
315
|
+
- Readiness rules for the two 2026-07-28 changes that have none yet: the
|
|
316
|
+
`Mcp-Method` / `Mcp-Name` request headers (SEP-2243), and Tasks moving out of
|
|
317
|
+
core into the `io.modelcontextprotocol/tasks` extension (SEP-2663).
|
|
260
318
|
- A public, continuously updated Agent Skills Quality Index over a broad corpus.
|
|
261
319
|
|
|
262
320
|
## Stability
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: efaimo
|
|
|
3
3
|
description: Audit MCP servers and Agent Skills for quality and context-window cost. Use when the user wants to check an MCP server's token cost, lint a skill, verify 2026-07-28 MCP spec readiness, or add an efaimo badge or CI gate.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.2"
|
|
7
7
|
homepage: "https://efaimo.ai"
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -18,6 +18,18 @@ skill well-formed", or "add a CI gate / badge for context cost".
|
|
|
18
18
|
|
|
19
19
|
Run via `npx efaimo` (no install needed).
|
|
20
20
|
|
|
21
|
+
**Before running any of these against a stdio server: auditing one means
|
|
22
|
+
executing it.** `weigh` and `check --mcp` start the command you pass as a child
|
|
23
|
+
process and speak MCP to it, because that is the only way to see the tool
|
|
24
|
+
definitions it would put in a context window. So only point them at commands
|
|
25
|
+
you would be willing to run yourself. A remote target (`https://...`) is not
|
|
26
|
+
executed and is the safe option for something you do not trust. `--client`
|
|
27
|
+
reads the MCP config files in the current directory as well as your user
|
|
28
|
+
config, so running it inside a repository you did not write means starting the
|
|
29
|
+
servers that repository configured. Until 2026-08-02 this warning lived only in
|
|
30
|
+
`SECURITY.md`, which was the wrong way round: the policy is where humans look,
|
|
31
|
+
and this file is where agents take orders.
|
|
32
|
+
|
|
21
33
|
- **Weigh context cost of an MCP server**
|
|
22
34
|
`npx efaimo weigh "npx -y <server-package>"` for a stdio server, or
|
|
23
35
|
`npx efaimo weigh https://host/mcp` for a remote one. Add `--json` for machine
|
package/dist/check/check.js
CHANGED
|
@@ -10,8 +10,12 @@ import { findSkills } from "../skills/parse.js";
|
|
|
10
10
|
import { weighServer, weighSkills } from "../weigh/weigh.js";
|
|
11
11
|
import { VERSION } from "../version.js";
|
|
12
12
|
function buildReport(surface, target, findings, notes) {
|
|
13
|
-
// MCP readiness findings (E101-E118) are a migration diff for
|
|
14
|
-
// not
|
|
13
|
+
// MCP readiness findings (E101-E118) are a migration diff for the 2026-07-28
|
|
14
|
+
// spec: a migration not yet made is a to-do list, not a defect in what
|
|
15
|
+
// shipped. Reported separately and never graded (ADR-014, reaffirmed in
|
|
16
|
+
// ADR-027). That decision is CLOSED: the spec published on 2026-07-28 and
|
|
17
|
+
// readiness still does not count toward a grade. This comment described it
|
|
18
|
+
// as open for a day after ADR-027, in the file that implements the split.
|
|
15
19
|
const readinessAll = [];
|
|
16
20
|
const gradedAll = [];
|
|
17
21
|
for (const f of findings) {
|
|
@@ -46,6 +50,14 @@ export async function checkMcpTarget(target, opts = {}) {
|
|
|
46
50
|
}
|
|
47
51
|
export function checkMcpRepoOnly(repoPath, label) {
|
|
48
52
|
const repo = scanRepo(repoPath);
|
|
53
|
+
// An empty result is a failure, never a pass. With no source files read and no
|
|
54
|
+
// dependency manifest naming an MCP SDK, there was nothing to examine here, and
|
|
55
|
+
// a clean A(100) would be a grade for a directory we never looked inside. Fail
|
|
56
|
+
// loudly, the way checkSkillSet does for a missing SKILL.md.
|
|
57
|
+
if (repo.filesScanned === 0 && !repo.sdk) {
|
|
58
|
+
throw new Error(`no MCP server source found under "${repoPath}": no code files and no SDK dependency. ` +
|
|
59
|
+
`Point --mcp at the server's run command, or at a directory that contains its source.`);
|
|
60
|
+
}
|
|
49
61
|
const emptyIntro = {
|
|
50
62
|
targetLabel: label,
|
|
51
63
|
transport: "stdio",
|
package/dist/cli.js
CHANGED
|
@@ -7,9 +7,11 @@ import { VERSION } from "./version.js";
|
|
|
7
7
|
import { resolveTarget } from "./targets/resolve.js";
|
|
8
8
|
import { loadClientServers, SUPPORTED_CLIENTS } from "./targets/clientConfigs.js";
|
|
9
9
|
import { introspectServer } from "./clients/introspect.js";
|
|
10
|
+
import { SPEC_VERSION } from "./clients/rawprobe.js";
|
|
10
11
|
import { weighServer, weighSkills } from "./weigh/weigh.js";
|
|
11
12
|
import { findSkills } from "./skills/parse.js";
|
|
12
13
|
import { diffServerWeigh } from "./weigh/diff.js";
|
|
14
|
+
import { DEFAULT_CONTEXT_WINDOW, formatWindowShare, setContextWindow } from "./weigh/window.js";
|
|
13
15
|
import { checkMcpRepoOnly, checkMcpTarget, checkSkillSet } from "./check/check.js";
|
|
14
16
|
import { renderCheckPretty, renderDiffPretty, renderScenarioPlan, renderServerWeighPretty, renderSkillSetPretty, renderSkillWeighPretty, renderTestReportPretty, setColor, } from "./reporters/pretty.js";
|
|
15
17
|
import { parseScenario, runScenario } from "./testing/harness.js";
|
|
@@ -45,7 +47,21 @@ function collectPairs(sep) {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
function colorSetup(opts) {
|
|
48
|
-
|
|
50
|
+
// Escape codes are noise once stdout is a file or another tool's stdin, and
|
|
51
|
+
// `efaimo weigh > report.txt` is a normal thing to do. So colour needs both
|
|
52
|
+
// the user's consent and a terminal to write to; FORCE_COLOR is the escape
|
|
53
|
+
// hatch for CI that renders ANSI in its log viewer.
|
|
54
|
+
const forced = process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "0";
|
|
55
|
+
const wanted = opts.color !== false && process.env.NO_COLOR === undefined;
|
|
56
|
+
setColor(wanted && (forced || process.stdout.isTTY === true));
|
|
57
|
+
}
|
|
58
|
+
function windowSetup(opts) {
|
|
59
|
+
if (opts.window === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const n = Number(opts.window);
|
|
62
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
63
|
+
fail(`--window must be a positive number of tokens, got "${opts.window}"`);
|
|
64
|
+
setContextWindow(n);
|
|
49
65
|
}
|
|
50
66
|
function fail(message) {
|
|
51
67
|
console.error(pc.red(`error: ${message}`));
|
|
@@ -66,12 +82,18 @@ function seconds(value, label = "--timeout") {
|
|
|
66
82
|
fail(`${label} must be a positive number of seconds, got "${value}"`);
|
|
67
83
|
return n;
|
|
68
84
|
}
|
|
69
|
-
function parseNumberOpt(value, label) {
|
|
85
|
+
function parseNumberOpt(value, label, bound) {
|
|
70
86
|
if (value === undefined)
|
|
71
87
|
return undefined;
|
|
72
88
|
const n = Number(value);
|
|
73
89
|
if (!Number.isFinite(n))
|
|
74
90
|
fail(`${label} must be a number, got "${value}"`);
|
|
91
|
+
// A budget or a percentage below its floor is not a stricter gate, it is a
|
|
92
|
+
// nonsensical one: `--max-tokens -5` turned "over budget" into always-true
|
|
93
|
+
// with a negative ceiling in the message. Reject it at the boundary.
|
|
94
|
+
if (bound && (bound.exclusive ? n <= bound.min : n < bound.min)) {
|
|
95
|
+
fail(`${label} must be ${bound.exclusive ? "greater than" : "at least"} ${bound.min}, got "${value}"`);
|
|
96
|
+
}
|
|
75
97
|
return n;
|
|
76
98
|
}
|
|
77
99
|
function anthropicKeyFor(opts) {
|
|
@@ -100,12 +122,14 @@ program
|
|
|
100
122
|
.option("--allow-increase <pct>", "with --diff: exit 1 if the increase exceeds pct percent")
|
|
101
123
|
.option("--badge [file]", "write an SVG badge + shields endpoint JSON")
|
|
102
124
|
.option("--anthropic [model]", "also measure exact Claude tokens via the count_tokens API (needs ANTHROPIC_API_KEY)")
|
|
125
|
+
.option("--window <tokens>", "context window the share is reported against", String(DEFAULT_CONTEXT_WINDOW))
|
|
103
126
|
.option("--no-color", "disable colors")
|
|
104
127
|
.action(async (targetArg, opts) => {
|
|
105
128
|
colorSetup(opts);
|
|
129
|
+
windowSetup(opts);
|
|
106
130
|
const timeoutMs = seconds(opts.timeout) * 1000;
|
|
107
|
-
const maxTokens = parseNumberOpt(opts.maxTokens, "--max-tokens");
|
|
108
|
-
const allowIncreasePct = parseNumberOpt(opts.allowIncrease, "--allow-increase");
|
|
131
|
+
const maxTokens = parseNumberOpt(opts.maxTokens, "--max-tokens", { min: 0, exclusive: true });
|
|
132
|
+
const allowIncreasePct = parseNumberOpt(opts.allowIncrease, "--allow-increase", { min: 0 });
|
|
109
133
|
const anthropic = anthropicKeyFor(opts);
|
|
110
134
|
const targets = [];
|
|
111
135
|
if (opts.client) {
|
|
@@ -114,6 +138,29 @@ program
|
|
|
114
138
|
fail(`no MCP servers found for client "${opts.client}" (checked: ${[...conf.sources, ...conf.missing].join(", ")})`);
|
|
115
139
|
}
|
|
116
140
|
console.error(pc.dim(`config: ${conf.sources.join(", ")}`));
|
|
141
|
+
// Say what is about to be EXECUTED, not just which file it came from.
|
|
142
|
+
//
|
|
143
|
+
// --client reads .mcp.json, .cursor/mcp.json and .vscode/mcp.json from
|
|
144
|
+
// the CURRENT DIRECTORY as well as the user config, and then spawns
|
|
145
|
+
// every command in them. Cloning an untrusted repo and running the audit
|
|
146
|
+
// tool inside it was therefore arbitrary code execution, and the only
|
|
147
|
+
// thing printed was the friendly config key, never the command. Cursor
|
|
148
|
+
// and VS Code gate project-scoped MCP config behind workspace trust;
|
|
149
|
+
// this printed nothing at all.
|
|
150
|
+
//
|
|
151
|
+
// Listing them is the minimum. A stdio entry names a real command that
|
|
152
|
+
// is about to run on this machine.
|
|
153
|
+
for (const e of conf.entries) {
|
|
154
|
+
const t = e.target;
|
|
155
|
+
const shown = t.kind === "stdio"
|
|
156
|
+
? [t.command, ...t.args].join(" ") + (t.env && Object.keys(t.env).length ? ` (+${Object.keys(t.env).length} env)` : "")
|
|
157
|
+
: t.url;
|
|
158
|
+
console.error(pc.dim(` ${e.name}: ${shown}`));
|
|
159
|
+
}
|
|
160
|
+
if (conf.fromCwd.length) {
|
|
161
|
+
console.error(pc.yellow(` ! ${conf.fromCwd.length} of these came from the current directory (${conf.fromCwd.join(", ")}), not your user config.\n` +
|
|
162
|
+
` Auditing a stdio server means executing it. Only continue in a repository you trust.`));
|
|
163
|
+
}
|
|
117
164
|
targets.push(...conf.entries.map((e) => e.target));
|
|
118
165
|
}
|
|
119
166
|
if (targetArg) {
|
|
@@ -186,7 +233,7 @@ program
|
|
|
186
233
|
.filter((r) => r.kind === "mcp")
|
|
187
234
|
.reduce((s, r) => s + r.totals.claudeStyle, 0);
|
|
188
235
|
if (mcpTotals > 0) {
|
|
189
|
-
console.log(pc.bold(`\ncombined MCP context cost (Claude-style, o200k est.): ${mcpTotals.toLocaleString("en-US")} tokens (
|
|
236
|
+
console.log(pc.bold(`\ncombined MCP context cost (Claude-style, o200k est.): ${mcpTotals.toLocaleString("en-US")} tokens (${formatWindowShare(mcpTotals)})`));
|
|
190
237
|
}
|
|
191
238
|
}
|
|
192
239
|
}
|
|
@@ -198,8 +245,14 @@ program
|
|
|
198
245
|
fail("--diff works on a single MCP server result");
|
|
199
246
|
const baselineRaw = JSON.parse(fs.readFileSync(opts.diff, "utf8"));
|
|
200
247
|
const baseline = ("data" in baselineRaw && baselineRaw.data ? baselineRaw.data : baselineRaw);
|
|
201
|
-
|
|
202
|
-
|
|
248
|
+
// A skill weigh baseline also carries a `totals` object, so a bare
|
|
249
|
+
// truthiness check passed it through and diffServerWeigh then died on the
|
|
250
|
+
// missing tool array with a raw TypeError. A server baseline is the one
|
|
251
|
+
// whose totals hold the three serializations; require the Claude-style
|
|
252
|
+
// total specifically so a skill baseline is rejected with a clear message.
|
|
253
|
+
if (!baseline?.totals || typeof baseline.totals.claudeStyle !== "number") {
|
|
254
|
+
fail(`"${opts.diff}" is not an MCP server weigh baseline; write one with: efaimo weigh <server> --out ${opts.diff}`);
|
|
255
|
+
}
|
|
203
256
|
const d = diffServerWeigh(baseline, single);
|
|
204
257
|
console.log("");
|
|
205
258
|
console.log(opts.md ? renderDiffMarkdown(d) : renderDiffPretty(d, { maxTokens, allowIncreasePct }));
|
|
@@ -226,6 +279,7 @@ program
|
|
|
226
279
|
.option("--repo <path>", "additionally scan this source repo for deprecated API usage")
|
|
227
280
|
.option("--no-probe", "skip live readiness probes (bare request, server/discover, ordering)")
|
|
228
281
|
.option("--strict", "exit 1 on warnings too")
|
|
282
|
+
.option("--strict-readiness", "exit 1 if the 2026-07-28 migration diff is not clean (never changes the grade)")
|
|
229
283
|
.option("--conformance", "after the audit, run the official MCP conformance suite (http targets)")
|
|
230
284
|
.option("--header <header>", 'HTTP header "Key: Value" (repeatable)', collectPairs(":"))
|
|
231
285
|
.option("--env <pair>", "KEY=VALUE for stdio servers (repeatable)", collectPairs("="))
|
|
@@ -234,9 +288,11 @@ program
|
|
|
234
288
|
.option("--md", "print Markdown")
|
|
235
289
|
.option("--badge [file]", "write a grade badge SVG + shields endpoint JSON")
|
|
236
290
|
.option("--anthropic [model]", "use exact Claude token counts where relevant")
|
|
291
|
+
.option("--window <tokens>", "context window the share is reported against", String(DEFAULT_CONTEXT_WINDOW))
|
|
237
292
|
.option("--no-color", "disable colors")
|
|
238
293
|
.action(async (targetArg, opts) => {
|
|
239
294
|
colorSetup(opts);
|
|
295
|
+
windowSetup(opts);
|
|
240
296
|
if (!targetArg)
|
|
241
297
|
fail("nothing to check: pass a target (see --help)");
|
|
242
298
|
if (opts.mcp && opts.skill)
|
|
@@ -305,10 +361,31 @@ program
|
|
|
305
361
|
console.error(pc.yellow("note: the official conformance suite drives http targets (--url); skipping for this target"));
|
|
306
362
|
}
|
|
307
363
|
else {
|
|
308
|
-
|
|
364
|
+
// The `latest` line of the conformance suite (0.1.16) predates the
|
|
365
|
+
// 2026-07-28 revision, so it silently tests the old protocol. Those
|
|
366
|
+
// scenarios ship on the `alpha` line only; --spec-version scopes them
|
|
367
|
+
// to the revision efaimo is about.
|
|
368
|
+
//
|
|
369
|
+
// Pinned to an exact version, because a dist-tag is not a pin. `alpha`
|
|
370
|
+
// moved from 0.2.0-alpha.9 to 0.2.0-alpha.10 on 2026-07-27, and we went
|
|
371
|
+
// on reporting "the official conformance suite" for a build nobody here
|
|
372
|
+
// had run: the same "we do not know what we ran" bug this block was
|
|
373
|
+
// written to fix, through the other door. Bumping it means re-reading
|
|
374
|
+
// `server --help` first; the two flags below were confirmed against
|
|
375
|
+
// 0.2.0-alpha.10.
|
|
376
|
+
const args = [
|
|
377
|
+
"-y",
|
|
378
|
+
"@modelcontextprotocol/conformance@0.2.0-alpha.10",
|
|
379
|
+
"server",
|
|
380
|
+
"--url",
|
|
381
|
+
target.url,
|
|
382
|
+
"--spec-version",
|
|
383
|
+
SPEC_VERSION,
|
|
384
|
+
];
|
|
385
|
+
console.error(pc.dim(`\nrunning official MCP conformance suite:\n npx ${args.slice(1).join(" ")}\n`));
|
|
309
386
|
// cross-spawn resolves npx(.cmd) without a shell, so target.url is passed
|
|
310
387
|
// as a literal argument (never interpreted by cmd.exe).
|
|
311
|
-
const r = crossSpawn.sync("npx",
|
|
388
|
+
const r = crossSpawn.sync("npx", args, {
|
|
312
389
|
stdio: "inherit",
|
|
313
390
|
});
|
|
314
391
|
if (r.status !== 0)
|
|
@@ -318,6 +395,15 @@ program
|
|
|
318
395
|
if (report.counts.error > 0 || (opts.strict && report.counts.warn > 0)) {
|
|
319
396
|
process.exitCode = 1;
|
|
320
397
|
}
|
|
398
|
+
// Readiness stays out of the grade, the badge and the default exit code
|
|
399
|
+
// (ADR-014, reaffirmed in ADR-027). But a team that HAS migrated had no way
|
|
400
|
+
// to stay migrated: --strict covers quality only, so nothing in CI could
|
|
401
|
+
// catch a regression back onto the legacy handshake. This is the opt-in for
|
|
402
|
+
// exactly that, and it moves the exit code only. Nobody's grade changes,
|
|
403
|
+
// no published number moves, and the operator decides rather than us.
|
|
404
|
+
if (opts.strictReadiness && (report.readiness?.findings.length ?? 0) > 0) {
|
|
405
|
+
process.exitCode = 1;
|
|
406
|
+
}
|
|
321
407
|
});
|
|
322
408
|
program
|
|
323
409
|
.command("test")
|