efaimo 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -4,6 +4,51 @@ 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
+ ## [0.4.0] - 2026-09-03
8
+
9
+ ### Fixed
10
+
11
+ - **`check --skill <dir>` finds skills it used to walk past**.
12
+ Two causes, both silent. The walk never entered a directory whose name starts
13
+ with a dot, so `.claude/skills/<name>/`, which is where a project keeps its
14
+ own skills, was invisible; and its depth bound of 3 missed layouts one level
15
+ deeper such as `skills/custom_skills/<name>/`. On the public corpus this
16
+ found 34 skills where `scripts/skills-index.mjs` found 38, and both numbers
17
+ were reported with confidence.
18
+
19
+ Dot directories are now entered for skill discovery and nowhere else, with
20
+ `.git` still excluded. The depth bound is 6 and is measured rather than
21
+ picked: the deepest real skill in that corpus sits 4 directories below a
22
+ directory of repositories and 3 below a repository root, so six leaves two
23
+ levels of headroom while still keeping an accidental `check --skill /` from
24
+ walking a disk.
25
+
26
+ **And when the bound does bite, it says so.** `SkillSet.truncatedAt` lists
27
+ the directories the walk refused to enter, because a bound that truncates in
28
+ silence is the same failure as a check that examines nothing. On the public
29
+ corpus it fires 16 times, all of them XML schema trees inside the `xlsx` and
30
+ `pptx` skills, which is the bound doing its job audibly.
31
+
32
+ - **One walker instead of two**. `scripts/skills-index.mjs`
33
+ carried its own copy with different rules, which is why the two disagreed.
34
+ It now calls the CLI's discovery, so they cannot drift apart again, and it
35
+ passes the truncation warning through. The published July index regenerates
36
+ byte-identically under the shared walk, which is how this was checked.
37
+
38
+ ### Added
39
+
40
+ - **S107: a filename one capitalisation away from a skill**.
41
+ Set-level and warn-level, so it moves no grade, in the same way S103 treats
42
+ a collision that belongs to a pair rather than to either member.
43
+
44
+ The spec names `SKILL.md` exactly and discovery matches it exactly, so
45
+ `skill.md` is not a skill here on any platform. It is not nothing anywhere:
46
+ a case-insensitive filesystem, the default on macOS and Windows, may hand it
47
+ to a host that opens it by name, so the same repository can carry a working
48
+ skill on the author's laptop and no skill at all in Linux CI. Nothing in
49
+ this tool would previously have mentioned such a file, which is the worst
50
+ property a near miss can have. Five exist in the public corpus.
51
+
7
52
  ## [0.3.0] - 2026-09-03
8
53
 
9
54
  ### Added
@@ -34,9 +79,17 @@ All notable changes to efaimo are documented here. Format follows
34
79
  both runs scored lower.
35
80
 
36
81
  Measured on the public skills corpus, 2026-07-17 pinned against 2026-09-03
37
- HEAD, same ruleset both sides: metadata -0.8%, body +7.5%, referenced files
38
- +61.4% with one skill at 96.5% of that movement; grades 0 improved and 4
39
- worsened.
82
+ HEAD, same ruleset both sides, over the **32 skills `check --skill` pairs**:
83
+ metadata -0.8%, body +7.5%, referenced files +61.4% with one skill at 96.5%
84
+ of that movement; grades 0 improved and 4 worsened.
85
+
86
+ The population matters and is worth stating, because
87
+ `research/skills-delta-2026-09-03/REPORT.md` reports body **+7.0%** over
88
+ **36** paired skills for the same corpus on the same dates. Both are true.
89
+ `scripts/skills-index.mjs` finds four skills that `check --skill` does not,
90
+ for the walker reason in Known below, so the two are measuring slightly
91
+ different populations. Every other figure is identical between them; the
92
+ body percentage is the one the four missing skills move.
40
93
 
41
94
  - **`efaimo find` takes several servers**. Pass more than one
42
95
  and their catalogs are merged into one and measured together, each tool
@@ -355,6 +408,7 @@ First release.
355
408
  Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
356
409
  and integration guide (`docs/INTEGRATIONS.md`).
357
410
 
411
+ [0.4.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.4.0
358
412
  [0.3.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.3.0
359
413
  [0.2.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.2.0
360
414
  [0.1.2]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.2
package/README.md CHANGED
@@ -406,3 +406,7 @@ Built by [efaimo ai](https://efaimo.ai), open tooling for the space between host
406
406
  and tools. `efaimo` is the flagship CLI; capabilities grow as subcommands under one
407
407
  name. Apache-2.0. See [CONTRIBUTING.md](./CONTRIBUTING.md) and
408
408
  [docs/RULES.md](./docs/RULES.md) to add a rule.
409
+
410
+ EFAIMO AI is a registered trademark of the publisher (KR 40-2585820, class 42).
411
+ The lowercase `efaimo ai` used throughout is the same mark; the package name and
412
+ the commands stay lowercase.
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, find out whether a search can single out a server's tools under deferred tool loading, or add an efaimo badge or CI gate.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- version: "0.3.0"
6
+ version: "0.4.0"
7
7
  homepage: "https://efaimo.ai"
8
8
  ---
9
9
 
@@ -85,5 +85,20 @@ export async function checkSkillSet(pathInput, label) {
85
85
  return { name, dir: skill.dir, report: buildReport("skill", name, findings, []) };
86
86
  });
87
87
  const setFindings = set.skills.flatMap((skill) => runRules(setRules, { skill, set, weigh }));
88
+ // S107 is emitted here rather than by a SkillRule because its subject is a
89
+ // file that is NOT a skill: no rule that takes a parsed skill could ever see
90
+ // it. Set level and warn level on purpose, so it moves no grade, matching
91
+ // how S103 treats a collision that belongs to a pair rather than to either
92
+ // member.
93
+ for (const file of set.miscasedSkillFiles ?? []) {
94
+ setFindings.push({
95
+ ruleId: "S107",
96
+ severity: "warn",
97
+ title: "filename is one capitalisation away from a skill",
98
+ message: `"${path.relative(set.root, file)}" is not loaded here, because the spec names SKILL.md exactly. A case-insensitive filesystem (the macOS and Windows default) may still hand it to a host, so this can work on a laptop and be missing entirely in Linux CI.`,
99
+ target: path.relative(set.root, file),
100
+ fixHint: "rename it to SKILL.md, or delete it if it is not a skill",
101
+ });
102
+ }
88
103
  return { label, root: set.root, perSkill, setFindings, weigh };
89
104
  }
@@ -339,6 +339,24 @@ export interface SkillInfo {
339
339
  export interface SkillSet {
340
340
  root: string;
341
341
  skills: SkillInfo[];
342
+ /**
343
+ * Directories the walk refused to enter because it hit its depth bound.
344
+ *
345
+ * Present only when the bound actually bit. A truncated search that reports
346
+ * a short list and no reason is the same failure as a check that examines
347
+ * nothing, so the caller is given the means to say what it did not look at.
348
+ */
349
+ truncatedAt?: string[];
350
+ /**
351
+ * Files one capitalisation away from being a skill, such as `skill.md`.
352
+ *
353
+ * The spec names `SKILL.md` exactly and discovery matches it exactly, so
354
+ * these are not skills here on any platform. They may still be skills to a
355
+ * host reading a case-insensitive filesystem, which is the default on macOS
356
+ * and Windows, so the same repository can work on a laptop and have no
357
+ * skills at all in Linux CI. Reported by S107.
358
+ */
359
+ miscasedSkillFiles?: string[];
342
360
  }
343
361
  export interface GradeInfo {
344
362
  score: number;
@@ -1,4 +1,10 @@
1
1
  import type { SkillInfo, SkillSet } from "../core/types.js";
2
2
  export declare function parseSkillFile(file: string): SkillInfo;
3
3
  /** Resolve a path (SKILL.md file, skill dir, or a directory of skills) into a SkillSet. */
4
+ /**
5
+ * How deep skill discovery walks. See the note inside `findSkills`: measured
6
+ * against the public corpus, where the deepest real skill is four directories
7
+ * below a directory of repositories.
8
+ */
9
+ export declare const SKILL_WALK_MAX_DEPTH = 6;
4
10
  export declare function findSkills(input: string): SkillSet;
@@ -129,6 +129,12 @@ export function parseSkillFile(file) {
129
129
  return base;
130
130
  }
131
131
  /** Resolve a path (SKILL.md file, skill dir, or a directory of skills) into a SkillSet. */
132
+ /**
133
+ * How deep skill discovery walks. See the note inside `findSkills`: measured
134
+ * against the public corpus, where the deepest real skill is four directories
135
+ * below a directory of repositories.
136
+ */
137
+ export const SKILL_WALK_MAX_DEPTH = 6;
132
138
  export function findSkills(input) {
133
139
  const abs = path.resolve(input);
134
140
  const stat = fs.statSync(abs);
@@ -139,11 +145,49 @@ export function findSkills(input) {
139
145
  if (fs.existsSync(direct)) {
140
146
  return { root: abs, skills: [parseSkillFile(direct)] };
141
147
  }
148
+ // Discovery is deliberate rather than incidental, because two walkers with
149
+ // different rules is what produced the 2026-09-03 defect: this found 34
150
+ // skills in a corpus where `scripts/skills-index.mjs` found 38, and both
151
+ // reported their own number with confidence.
152
+ //
153
+ // maxDepth 6 is measured, not picked. In the public corpus the deepest real
154
+ // skill sits 4 directories below a directory-of-repositories
155
+ // (`<corpus>/<repo>/skills/custom_skills/<name>/SKILL.md`) and 3 below a
156
+ // repository root. Six leaves two levels of headroom and keeps an accidental
157
+ // `check --skill /` from walking a disk. When the bound does bite it is
158
+ // reported rather than swallowed, in `truncatedAt`.
159
+ //
160
+ // Dot directories are entered here and nowhere else in this codebase:
161
+ // `.claude/skills/<name>/` is where a project keeps its own skills, so
162
+ // skipping them made this blind to the most common real layout. `.git` is
163
+ // still excluded by DEFAULT_SKIP_DIRS.
142
164
  const skills = [];
143
- for (const f of walkFiles(abs, { maxDepth: 3 })) {
144
- if (path.basename(f) === "SKILL.md")
165
+ const truncatedAt = [];
166
+ const miscased = [];
167
+ for (const f of walkFiles(abs, {
168
+ maxDepth: SKILL_WALK_MAX_DEPTH,
169
+ includeDotDirs: true,
170
+ onDepthLimit: (d) => truncatedAt.push(d),
171
+ })) {
172
+ const base = path.basename(f);
173
+ if (base === "SKILL.md")
145
174
  skills.push(parseSkillFile(f));
175
+ // A file that is only a capitalisation away from being a skill. The spec
176
+ // names SKILL.md exactly, and this comparison is exact, so `skill.md` is
177
+ // invisible here on every platform. It is not invisible everywhere: a
178
+ // case-insensitive filesystem, which is the default on macOS and Windows,
179
+ // may hand it to a host that opens it by name, so the same repository has
180
+ // a working skill on the author's laptop and no skill at all in Linux CI.
181
+ // Nothing else in this tool would ever mention it, which is the worst
182
+ // property a near miss can have.
183
+ else if (base.toLowerCase() === "skill.md")
184
+ miscased.push(f);
146
185
  }
147
186
  skills.sort((a, b) => a.file.localeCompare(b.file));
148
- return { root: abs, skills };
187
+ return {
188
+ root: abs,
189
+ skills,
190
+ ...(truncatedAt.length ? { truncatedAt } : {}),
191
+ ...(miscased.length ? { miscasedSkillFiles: miscased.sort() } : {}),
192
+ };
149
193
  }
@@ -3,10 +3,28 @@ export declare function parseCommandString(input: string): {
3
3
  command: string;
4
4
  args: string[];
5
5
  };
6
- export declare function walkFiles(root: string, opts?: {
6
+ export interface WalkOptions {
7
7
  maxDepth?: number;
8
8
  skipDirs?: Set<string>;
9
- }): Generator<string>;
9
+ /**
10
+ * Descend into directories whose name starts with a dot.
11
+ *
12
+ * Off by default, because most of them are caches and metadata. On for skill
13
+ * discovery, because `.claude/skills/<name>/` is where a project keeps its
14
+ * own skills and a tool that cannot see the most common real layout is not
15
+ * auditing anything. `DEFAULT_SKIP_DIRS` still excludes `.git`.
16
+ */
17
+ includeDotDirs?: boolean;
18
+ /**
19
+ * Called once for each directory not entered because `maxDepth` was reached.
20
+ *
21
+ * A bound that truncates in silence is the same failure as a check that
22
+ * examines nothing: the caller sees a short list and no reason for it. This
23
+ * lets the caller say so.
24
+ */
25
+ onDepthLimit?: (dir: string) => void;
26
+ }
27
+ export declare function walkFiles(root: string, opts?: WalkOptions): Generator<string>;
10
28
  export declare function readTextSafe(file: string, maxBytes?: number): string | undefined;
11
29
  export declare function isBinaryPath(file: string): boolean;
12
30
  export declare function extractVersion(s: string): number[] | undefined;
package/dist/util/misc.js CHANGED
@@ -59,9 +59,15 @@ export function* walkFiles(root, opts = {}) {
59
59
  for (const e of entries) {
60
60
  const full = path.join(dir, e.name);
61
61
  if (e.isDirectory()) {
62
- if (depth < maxDepth && !skip.has(e.name) && !e.name.startsWith(".")) {
63
- stack.push({ dir: full, depth: depth + 1 });
62
+ if (skip.has(e.name))
63
+ continue;
64
+ if (!opts.includeDotDirs && e.name.startsWith("."))
65
+ continue;
66
+ if (depth >= maxDepth) {
67
+ opts.onDepthLimit?.(full);
68
+ continue;
64
69
  }
70
+ stack.push({ dir: full, depth: depth + 1 });
65
71
  }
66
72
  else if (e.isFile()) {
67
73
  yield full;
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in sync with package.json by test/meta.test.ts. */
2
- export declare const VERSION = "0.3.0";
2
+ export declare const VERSION = "0.4.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in sync with package.json by test/meta.test.ts. */
2
- export const VERSION = "0.3.0";
2
+ export const VERSION = "0.4.0";
package/docs/RULES.md CHANGED
@@ -65,7 +65,7 @@ never reused, so a gap is permanent and is better stated than smoothed over.
65
65
  | E128 | warn | total tool-definition tax over ~10k / ~25k tokens, estimated (needs `weigh` data); hard CI failure is left to the `--max-tokens`/`--diff` gate |
66
66
  | E130 | info | instruction-injection patterns in a tool description or server instructions (shallow heuristic, never a security verdict; use a dedicated scanner for depth). **Reported but never scored** |
67
67
 
68
- ## Skills (S101-S106)
68
+ ## Skills (S101-S107)
69
69
 
70
70
  | id | sev | what it catches | spec |
71
71
  |---|---|---|---|
@@ -75,6 +75,7 @@ never reused, so a gap is permanent and is better stated than smoothed over.
75
75
  | S104 | warn/info | metadata over ~100 tokens, body over ~5k tokens or 500 lines, or long body with no progressive disclosure | agentskills.io |
76
76
  | S105 | info | instruction-injection patterns in the skill body or description (shallow heuristic, never a security verdict). **Reported but never scored** | internal |
77
77
  | S106 | error/warn/info | referenced file missing, escaping the skill dir (`..`), or nested more than one level deep | agentskills.io |
78
+ | S107 | warn | a file one capitalisation away from a skill, such as `skill.md`. Set-level and never scored: the spec names `SKILL.md` exactly, so it is not a skill here on any platform, but a case-insensitive filesystem (the macOS and Windows default) may hand it to a host, which makes a repository work on a laptop and have no skill at all in Linux CI | internal |
78
79
 
79
80
  ## Findability (E141-E145)
80
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "efaimo",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Audit what your agent loads: quality and context cost for MCP servers and Agent Skills",
5
5
  "keywords": [
6
6
  "mcp",