Skip to content

test(ci): Enforce that knowledge/ installations resolve - #87

Open
MajorLift wants to merge 4 commits into
mainfrom
jongsun/fix/knowledge-installs-once-per-domain
Open

test(ci): Enforce that knowledge/ installations resolve#87
MajorLift wants to merge 4 commits into
mainfrom
jongsun/fix/knowledge-installs-once-per-domain

Conversation

@MajorLift

@MajorLift MajorLift commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Guards a property that breaks silently: every knowledge/… reference in an installed skill must resolve on disk.

The hazard

Domain knowledge/ is copied beside every skill in the domain, so an installed skill body reaches it as knowledge/<file>.md. In this repo the same file sits two levels above the skill. The two layouts disagree:

repo        domains/<area>/knowledge/x.md          domains/<area>/skills/<s>/skill.md
installed   .claude/skills/mms-<s>/knowledge/x.md  .claude/skills/mms-<s>/SKILL.md

So a repo-relative citation (../../knowledge/x.md) is correct in the source tree and broken in the delivered output — and nothing reports it. Neither does the reverse. Any change to where the installer places knowledge has the same effect on every skill that cites it: the reference dangles, the skill still installs, and the agent quietly cannot find what the body points at.

domains/perps alone carries 13 such citations.

What ships

A guard (test/cli.test.mjs L244–L309, commit-pinned) — installs a domain and asserts that every knowledge/… reference in each emitted skill resolves on disk, across all three operator outputs.

Its fixture carries a real consumer: a skill body that cites a knowledge file the way shipped skills do. This is the load-bearing part. A fixture without a citation cannot exhibit a placement regression at all — every assertion in it passes while delivered references break.

A corpus check — the guard above proves the installer places knowledge correctly. It says nothing about whether the skills in this repo cite files their own domain actually ships. A second test sweeps every knowledge/… citation in domains/ and fails on any that its domain cannot deliver.

It found six that already dangle, all one bug: four skills in coding, perps, and pr-workflow cite knowledge/testing-layers.md, which lives in domains/testing/. Cross-domain knowledge has no delivery mechanism, so those references cannot resolve for any consumer. They are listed in KNOWN_UNRESOLVED so this lands green and blocks new breakage instead of merging red; a companion test fails if a listed citation starts resolving, so the list can only shrink. Tracked in #88.

Docs — README and CONTRIBUTING state the rule the layout difference forces: cite knowledge by name, or by the installed-relative path, never a repo-relative one.

Evidence

A guard that cannot fire is not a guard. Run against an installer that places knowledge somewhere other than beside the skill, it fails with the dangling reference named:

knowledge placed elsewhere    ✖ every knowledge/ reference resolves — 11 pass / 1 fail
    AssertionError: .claude/skills/mms-consumer: dangling knowledge reference
    knowledge/alpha.md — the body cites it but install did not place it there

knowledge beside the skill    ✔ 12 pass / 0 fail

Notes

  • Docs and tests only; no behavior change.
  • The corpus check ships with six pre-existing citations allowlisted (Skills cite knowledge files their domain does not ship (cross-domain knowledge is unsupported) #88). Both tests were verified to fire: a new dangling citation fails one naming the offending pair, and satisfying a listed citation fails the other.
  • The guard is what makes the delivered layout safe to revisit. Any future change to knowledge placement — see the appendix — has to keep it green.

Appendix — why not a symlink

Knowledge is copied beside every skill, so a domain with K files and N skills delivers K×N — perps ships 108 where 27 would do. A per-skill symlink into one shared directory is the obvious fix. It does not hold up, on three counts.

It deduplicates nothing that matters. A recursive walk following links sees the content once per skill plus once at the shared location:

$ find multi -L -name '*.md'      # 2 skills, 1 shared knowledge file
multi/shared/k.md
multi/s2/knowledge/k.md
multi/s1/knowledge/k.md

Same N× duplication a copy produces, for an agent reading the tree. The symlink saves disk — the cheap resource, regenerated every sync — and saves nothing in the context window, the scarce one.

Relocation strands it. The link is relative, so copying an installed skill anywhere else breaks it:

$ cp -R skill elsewhere/skill
$ node -e "console.log(fs.existsSync('elsewhere/skill/knowledge/alpha.md'))"
false          # still points at ../shared, which is not there

Copying a single mms-<skill>/ directory elsewhere is an ordinary thing to do.

Windows fails as a wrong-type file, not an error. Git stores symlinks as mode 120000; where symlink creation is unavailable — no Developer Mode, no elevation, core.symlinks=false — git checks out a regular file containing the target path. knowledge becomes a file, so knowledge/alpha.md is untraversable while existsSync on the parent returns true. Not hypothetical for every consumer: core does not ignore the install target, so output there is committable and can reach a Windows contributor through git. metamask-extension does ignore it.

This repo ships no symlinks — git ls-files -s reports no mode-120000 entries — so it would be a new artifact class in the delivered tree.

Conclusion. Copying is right: the installed tree is generated, the duplication costs only disk, and every alternative trades that for a silent failure mode. If deduplication is revisited, rewriting citations as the installer emits each skill is the safer direction — it changes text, not filesystem semantics, and the guard here would catch it going wrong.

`copy_domain_knowledge` ran per skill per operator, so a domain with K knowledge
files and N skills wrote K*N copies each. `perps` shipped 108 files where 27 were
needed; `performance` after the pending domain PRs would ship 168 for 21.

Knowledge now installs once to `mms-<domain>-knowledge/`, a sibling of the
domain's installed skills. An upgrade removes the per-skill copies an older
install left behind, and the shared directory is registered as expected so
`--prune-stale` leaves it alone.

This also fixes cross-layer references. The per-skill copy sat as a sibling of
`references/`, three levels from where it lives in the repo, so
`../../../knowledge/x.md` resolved in the repo and broke once installed while
`../knowledge/x.md` did the reverse — no relative path was correct in both, and
nothing reported the breakage. README and CONTRIBUTING now state the rule: cite
knowledge files by name, never by relative path.
Installing domain `knowledge/` once per domain deduplicated the delivered tree
but stranded every skill-relative `knowledge/<file>.md` citation — 12 working
references in `domains/perps` alone. The installed tree is generated on every
sync, so the duplication it removed was not worth a breaking layout change.

`tools/install` is restored byte-for-byte to its previous behavior. What remains
is the part that had value independent of the layout:

- A regression guard: every `knowledge/…` reference in an emitted skill must
  resolve on disk after install. Verified to FAIL against the reverted design
  with `dangling knowledge reference knowledge/alpha.md`, and to pass here — a
  guard that cannot fire is not a guard.
- Its fixture carries a real consumer (a skill body that cites a knowledge file),
  because the previous fixture had none and was structurally unable to exhibit
  the regression while every assertion passed.
- README and CONTRIBUTING now state the rule the layout difference forces: cite
  knowledge by name or by the installed-relative path, never a repo-relative one,
  which is broken in the delivered output with nothing reporting it.
@MajorLift MajorLift changed the title fix(cli): install domain knowledge/ once per domain, not once per skill test(cli): guard that installed knowledge references resolve Jul 30, 2026
The fixture guard proves `tools/install` places knowledge where a skill body
expects it. It says nothing about whether the skills in this repo cite files
their own domain actually ships — and six citations do not.

`knowledge/` is copied per domain, so a skill can only cite its own domain's
files. Four skills in `coding`, `perps`, and `pr-workflow` cite
`knowledge/testing-layers.md`, which lives in `domains/testing/`. The installer
has no way to deliver it into those domains, so the reference cannot resolve for
any consumer on any operator. Five of the six sit in `repos/metamask-mobile.md`
overlays, which is likely why they went unnoticed.

Those six are listed in `KNOWN_UNRESOLVED` so the check lands green and blocks
new breakage rather than merging red. A second test fails if an entry starts
resolving, so the list can only shrink.

Both directions verified to fire: a new dangling citation fails the first test
naming the offending pair, and satisfying a listed citation fails the second.
@MajorLift MajorLift changed the title test(cli): guard that installed knowledge references resolve test(ci): Enforce that knowledge/ references resolve on disk Jul 30, 2026
@MajorLift MajorLift changed the title test(ci): Enforce that knowledge/ references resolve on disk test(ci): Enforce that knowledge/ installations resolve Jul 30, 2026
Two checks that found real defects by hand, now standing. Both run under
`yarn test`, need no network, and pass on the current corpus, so they gate new
breakage rather than landing red.

Personal references — this repo is public, so an absolute home path, a personal
handle, or a private-repo name is both a leak and a reference no reader but its
author can resolve. The path pattern is anchored to a boundary; an unanchored
one matches `../pages/home/homepage`.

Frozen-branch links — `metamask-extension` moved its default to `main`, but
`develop` still exists with a last commit of 2026-01-15. Links to it load and
serve stale source, which is worse than a 404 because nothing signals the age.
`FROZEN_BRANCHES` is a list so more can be added as branches are retired.

Both verified to fire on an injected violation, naming file, line, and reason.

Deliberately not gated: requiring every `/blob/<branch>/` link to be SHA-pinned
fires 19 times on existing content, and is the wrong rule anyway — a directory
listing should track the default branch. Pin when a link is evidence for a
claim; track the branch when it is a place to look.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds CI-level protections and documentation to prevent silent breakage where installed skills reference knowledge/… files that aren’t actually present in the installed output, and to validate that in-repo knowledge/… citations resolve within their own domain.

Changes:

  • Adds an install-time guard test that installs a fixture domain and asserts knowledge/… references in emitted skill outputs resolve on disk.
  • Adds a corpus-wide test that scans skills for knowledge/… citations and fails on any that the skill’s own domain cannot deliver (with an allowlist for known cross-domain cases).
  • Documents the correct way to refer to domain knowledge files in skills (installed-relative knowledge/<file>.md or by name; never repo-relative).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
test/cli.test.mjs Adds new test guards for installed knowledge reference resolution and corpus validation of knowledge citations.
README.md Documents the knowledge layout difference between repo and installed output and the safe citation formats.
CONTRIBUTING.md Adds contributor guidance linking to the README rule for knowledge citations.
Suppressed comments (1)

test/cli.test.mjs:340

  • The corpus citation sweep currently only detects knowledge references in Markdown links or backticks. There are existing plain-text references like knowledge/testing-layers.md (e.g. in domains/testing/skills/unit-testing/skill.md) that won’t be validated, so new dangling citations in that format could slip through. Expand the matcher to also catch bare knowledge/<file>.md occurrences.
          for (const m of body.matchAll(/\]\((knowledge\/[\w.-]+\.md)\)|`(knowledge\/[\w.-]+\.md)`/gu)) {
            const ref = m[1] || m[2];
            found.push({ rel, domain, ref, key: `${rel} → ${ref}` });
          }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/cli.test.mjs
Comment on lines +298 to +300
const body = readFileSync(path.join(skillDir, file), 'utf8');
const refs = [...body.matchAll(/\]\((knowledge\/[\w.-]+)\)/gu)].map((m) => m[1]);
assert.ok(refs.length > 0, `${base}/${name}: expected a knowledge reference in the emitted body`);
Comment thread test/cli.test.mjs
Comment on lines +285 to +287
'bash',
[INSTALL, '--target', target, '--repo', 'core', '--source', source],
{ encoding: 'utf8' },
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants