Skip to content

fix(cli): Ensure knowledge/ citations and cross-skill references resolve - #87

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

fix(cli): Ensure knowledge/ citations and cross-skill references resolve#87
MajorLift wants to merge 16 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, and then makes it hold: every knowledge/… reference in an installed skill body resolves on disk. The installer now delivers knowledge a skill cites from another domain, and new tests keep it that way at the installer and across the source tree.

The hazard

The installed layout puts knowledge two levels below where this repo keeps it, so every citation is written against one placement or the other. Before this change that broke in two ways, neither of which reported anything.

Wrong shape. A repo-relative citation (../../knowledge/x.md) is correct in the source tree and broken in the delivered output, and the reverse holds too. So any change to where the installer puts knowledge breaks every skill that cites it, with nothing reporting it before this change — and domains/perps/skills alone carries 13 knowledge citations.

Never delivered. Knowledge was copied per domain, so a skill citing another domain's file received nothing at all. Six citation sites across four skills in coding, perps and pr-workflow cite knowledge/testing-layers.md, which lives in domains/testing/. Five of the six sit in repos/metamask-mobile.md overlays, which is plausibly why the breakage went unnoticed.

Before this change, either way the skill installed, the reference dangled, and the agent quietly could not open what the body pointed at.

What ships

The cross-domain copy (tools/install L376–L442, tests at test/cli.test.mjs L545–L629, commit-pinned) — the installer scans a skill for knowledge/<file>.md citations its own domain cannot satisfy and copies the file in from whichever domain owns it. Existing citations start working unchanged — no new syntax to learn, and nothing is declared in frontmatter, so the context an agent must load stays bounded. A cross-domain reference stays a body link the agent chooses to follow.

Overlays are scanned too. Scanning only skill.md reaches one of the four affected skills; the other three cite testing-layers.md from a repos/… overlay.

Ambiguity fails loudly. Resolution is by filename — resolve_foreign_knowledge globs */knowledge/<name> across domains/, and the name is unambiguous across the eleven knowledge files in domains/ on this branch. If that stops being true the reference is genuinely ambiguous, so the install exits non-zero and names the candidates rather than picking one.

The installer guard (test/cli.test.mjs L284–L365) — installs a fixture 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 knowledge the way shipped skills do, in all three forms a citation takes — markdown link, inline code, bare prose. A fixture without a citation cannot exhibit a placement regression at all, and a fixture carrying only the form the matcher already handles cannot exhibit the matcher's blind spot, because the fixture and the matcher share it.

The set of references found is asserted for the same reason: a narrowed matcher finds a subset, every member of the subset resolves, and a resolves-only assertion stays green while coverage shrinks.

A corpus check (test/cli.test.mjs L367–L452) — sweeps every knowledge/… citation in domains/ and fails on a repo-relative path, or on a file the citing skill's own domain does not ship. The installer guard proves the installer places knowledge correctly; it says nothing about whether the skills in this repo cite files their own domain ships.

The testing-layers.md citations are listed in the corpus check's KNOWN_UNRESOLVED allowlist even though the installer now delivers them, because delivery and citation hygiene are different properties. The corpus rule is a source-tree convention — cite your own domain's knowledge, so the citation reads without knowing the installer. Emptying the allowlist means moving testing-layers.md into each citing domain, or agreeing cross-domain citation is fine and deleting the corpus check. Both are calls for #88 (skills cite knowledge files their domain does not ship), not for this PR.

A companion test in the same block, at L441–L451, fails on any entry that starts resolving, so the allowlist can only shrink.

Two publication gates (test/cli.test.mjs L454–L543) — one rejects a personal path, handle, or private-repo name in a skill, which in a public repo fails twice over: it leaks, and it is a dead reference for every reader but its author.

A second rejects links into develop, which is worse than a deleted branch: the link loads and hands the reader stale source with no signal. metamask-extension moved to main; develop still resolves and last took a commit on 2026-01-15.

The private identifiers themselves are not listed in the test. A denylist of private identifiers, committed to a public repo, publishes every identifier it protects. Structural patterns describing a shape are inline; anything naming a particular person or host is derived from the environment (GITHUB_ACTOR in CI, the git identity locally), with SKILLS_PRIVATE_PATTERNS available to add more from a secret or an untracked file.

DocsREADME 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

The figures in the table below come from one scenario: --repo metamask-mobile --domain coding,perps,pr-workflow,testing, run against this branch's domains/ tree, counting every knowledge/… occurrence in the emitted .claude/skills/*/SKILL.md bodies — sites, not distinct targets, and only across the skills that selection emits, which is fewer than domains/ holds. The only variable is which tools/install runs.

tools/install from citations that resolve
merge base 4b00c7d, whose tools/install is the same blob main has today 3 / 9
this branch 9 / 9

The six that were dangling are the testing-layers.md sites in coding-guidelines (×3), perps-review-pr, pr-guidelines and pr-readiness-check. All four skills receive the file in each of the three outputs, and none did before — the same run writes .cursor/rules (as RULE.md) and .agents/skills, and both count 3 / 9 → 9 / 9 too.

A guard that cannot fire is not a guard, so all nine of the test cases this PR adds were run against a defect they should catch, across the eight runs below — the first defect trips two of them. The suite is 74 tests and passes 74/74 (yarn test, i.e. node --test test/*.test.mjs) with nothing induced. Each row below is a separate run of the full suite against this branch, with one defect planted in the tree.

defect induced result
installer places knowledge at kb/ instead of beside the skill 72/74 — the installer guard and the owning domain still gets its own knowledge both fail: dangling knowledge reference knowledge/alpha.md — the body cites it but install did not place it there
cross-domain resolution returns nothing 73/74 — .claude/skills: cross-domain knowledge not delivered
the ambiguity check deleted, two domains shipping one filename 73/74 — ambiguous reference should fail the install
testing-layers.md copied into domains/coding/knowledge/, so a listed citation resolves 73/74 — these citations now resolve — remove them from KNOWN_UNRESOLVED
a new cross-domain citation added to a coding skill 73/74 — new dangling knowledge citation(s) — a skill may only cite its own domain's knowledge
a ../../knowledge/… path planted in one skill 73/74 — a `../knowledge/…` path resolves in this repo and 404s once installed
a /home/<user>/… path planted in one skill 73/74 — personal or private references must not ship in a public skill
a blob/develop/ link planted in one skill 73/74 — link points into a frozen branch — use the repo's default branch, or pin a SHA

The corpus check caught a real violation on main — in a skill this PR does not touch — and it is fixed. main merged #130 (unified extension-testing skill), whose extension-testing skill cited a second domains/testing/ knowledge file, extension-testing-layers.md, repo-relatively — skill.md as ../../knowledge/…, references/layers.md as ../../../knowledge/… — exactly the form the corpus check bans, so one test failed, naming four citations across two files. Dropping the prefix in both files clears all four, the same edit 004d5daCite knowledge/testing-layers.md without the repo-relative prefix already made for testing-layers.md; that is what 4797d98Merge main, and fix the citations it introduced does, and the run on that commit reports the four citations resolved.

Notes

  • This changes installer behavior. Say the word if you want a CHANGELOG.md entry, and which release it should land under.
  • Scope of the property. Both the installer's scan and the guard read a skill's emitted body. A citation inside a delivered knowledge/ or references/ file is out of scope for both — domains/perps/knowledge/review-antipatterns.md L111 cites knowledge/testing-layers.md and still dangles for every perps skill except perps-review-pr, which cites the file in its own body and so receives it anyway. Widening the scan to delivered files is a one-line change to sources=(…), and belongs in a follow-up rather than here.

Appendix — why not a symlink

A --repo metamask-mobile install writes 57 copies of knowledge into perps' two installed skills across the three operator outputs — 54 from the domain's own 9 files, plus 3 copies of the testing-layers.md this PR now delivers. 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

The installed tree is regenerated wholesale on every sync, so a copied mms-<skill>/ is the only form of it that outlives one install — and that copy is exactly the one a symlink breaks.

Windows fails as a wrong-type file, not an error. Git stores a symlink as a mode-120000 entry, so where symlink creation is unavailable — no Developer Mode, no elevation, core.symlinks=false — it checks out as a regular file containing the target path. knowledge becomes a file, so knowledge/alpha.md is untraversable while existsSync on the parent returns true: a failure that reads as success at the only level anything checks.

All three consumers ignore the install target as of 2026-08-20: core, metamask-extension and metamask-mobile each gitignore .claude/skills/ and .agents/skills/, and .cursor/rules/ — as .cursor/rules/* with an *.mdc carve-out on mobile, which still excludes every directory the installer writes. So this would arrive through the installer rather than through a checkout, which narrows who is exposed without changing the failure mode.

This repo also 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. The cross-domain copy above extends it rather than replacing it, for the same reasons. If deduplication is revisited, rewriting citations as the installer emits each skill is the safer direction — it changes text, not filesystem semantics, and the installer guard would catch it going wrong.

@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
@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
@MajorLift
MajorLift marked this pull request as ready for review July 30, 2026 18:03
Three reference kinds could name something no reader can reach, and none were
checked:

- Lane ids (`B7`, `C4`) are addresses into `evidence-catalog.md`, not names.
  A `description` cannot link out to the catalog at all, so a lane id there is
  unresolvable by construction — that is an error. In the body it is a warning
  unless the line links the catalog.
- `[[snake_case]]` wiki links come from a private authoring vault and render as
  literal brackets here. Matching on the underscore keeps JS array literals
  (`[[signer1.address, …]]`) from tripping the rule.
- Names in `## Related` must resolve to a skill. A warning rather than an error:
  the gate runs on the PR's own branch, where a sibling skill shipping in a
  concurrent PR does not exist yet.

`collectSkills` takes an array of roots; passing a bare string iterates its
characters and yields zero skills, which would have made every check above pass
vacuously.

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 Outdated
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`);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: 7adfa62

Comment thread test/cli.test.mjs Outdated
…nymously

This repo is public, so naming a repository discloses that it exists, who owns it and
roughly what it holds. A prohibition discloses exactly as much as a recommendation —
"do not re-host to <personal repo>, it is private" publishes the name either way — so
the rule is about the mention, not the sentiment attached to it. A skill shipped with
a personal repo named in a warning, on that reasoning.

The check is a request rather than a list. An owner allowlist was tried first and was
wrong on its first run: it flagged `nock/nock` and `phishfort/phishfort-lists`, because
"is this owner well known" is not the property that matters. The property is whether a
reader who is not you can open the link, and an unauthenticated HEAD answers it exactly.
Deliberately unauthenticated — a token would see private repos and pass them.

`MetaMask` and `Consensys` are exempt: org repos are unreachable publicly but readable
by colleagues, and naming them is a deliberate call rather than a leak.

Two-arm verified: fires on `MajorLift/Reprise` in the tree that shipped it, silent on
the tree with it removed. The first version scanned only skill.md and would have passed
the violation that motivated it, which was in a references/ file.
Scoped to the whole repository rather than the PR's changed files: whether this repo
names something a reader cannot open is a property of what it publishes, not of what a
given PR touched, and the reference that motivated this had sat unnoticed through several
PRs that edited the same file.

`GH_TOKEN` and `GITHUB_TOKEN` are blanked for the step. The check must run as a stranger —
an authenticated request resolves private repositories and passes them, which is the exact
failure it exists to catch.
"Did the right skill load" is a question about a probabilistic event — a description
matched by a model — so the only honest answer comes from the transcript rather than
from the description. This reads one and reports what entered context, by route.

Three routes leave three different traces (Skill tool call, slash command, and the
loader's base-directory announcement on a description match). Counting one of them reads
as silence: run against the session that produced this file, the Skill-tool count alone
suggests the reasoning skills were used, and all three together show that not one of
them loaded at any point.

The second report is the deterministic one. A publish is UNGATED if no gate ran before it
at all, and UNCHAINED if a gate ran earlier but not as the same command. The distinction
is the finding: a gate that merely ran earlier proves nothing, because the verdict can be
read after the write — which is exactly how a blocked artifact reached a public pull
request in the session this was written from. Only `gate && publish` makes the shell
enforce the dependency.

The first version reported that session as clean on the generous rule. It is 195
unchained publishes on the strict one.
…sees

The lane-id, wiki-link and `## Related` checks had no cases in a suite that
already exercises this script heavily, so each was one edit away from silently
matching nothing.

Ten cases, each shown to fail against the behaviour it claims to guard rather
than merely to pass. Six mutations, five caught by exactly the test that names
them: offset zeroed, catalog-link suppression removed, catalog-owner exemption
removed, wiki regex loosened to any token, `## Related` running to EOF.

The sixth is the interesting one. Deleting `&& name !== skill.name` changes
nothing, because the linted skill is collected from the same tree as the
known-name set — its own name is always in that set, so the clause never decides
the case it appears to. That test is kept for the behaviour and its comment now
says which mechanism actually delivers it.

The offset assertion derives the expected line by scanning the written file, not
by repeating the linter's arithmetic. The frontmatter it strips is exactly what
shifts the numbers, so a test that recomputed the offset the same way would agree
with the defect it exists to catch.

71 pass / 0 fail; repo lint 0 errors.
`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.
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.
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.
The denylist named five of them — a home path, a handle, and three repository names —
committed to a public repository. A denylist of private identifiers publishes every
identifier it protects, so the test leaked precisely what it existed to prevent, and
did so more completely than any single skill file had.

Structural patterns describe a shape and stay inline: an absolute `/home` or `/Users`
path, an ssh remote. Anything naming a particular person, host or repository now comes
from `SKILLS_PRIVATE_PATTERNS` — a newline-separated list of regex sources supplied by
CI secret or an untracked local file, so the corpus is checked without the corpus being
published.

A generic email pattern was tried and dropped: it fired on a third-party address in
oh-my-opencode's documented config, which is a documentation example rather than a leak.
Identity-shaped patterns belong in the configured list, where the person who owns the
identity decides.

Two-arm verified: fails on a planted reference with the pattern configured, passes with
the reference removed.
Moving the denylist to `SKILLS_PRIVATE_PATTERNS` stopped the leak and stopped the check:
nothing sets that variable, so the identifier arm matched nothing anywhere. A check that
cannot fire is not a weaker check, it is an absent one wearing the name of a check.

The identifiers most likely to leak belong to whoever is running, and the environment
already knows who that is — `GITHUB_ACTOR` in CI, `USER` and `git config user.name`
locally. Values shorter than four characters or on a generic list (runner, ubuntu, ci,
node…) are dropped, since a two-letter username matches every file. `SKILLS_PRIVATE_PATTERNS`
remains for anything else worth catching.

Two-arm verified with no environment configured: fails on a planted reference to the
running user's handle, passes with it removed.
The guard read the delimiter, not the citation. Anchoring `knowledge/` to a
preceding `](` or backtick matched two of the three forms skills actually use,
and — because the `../` sits between the delimiter and the path — none of the
repo-relative ones. That is the form the README calls broken and says this test
guards, so the one defect the check existed to catch was the one it could not
express.

`perps-write-ticket` carried two, under a heading reading "read installed".
Both 404 in the installed tree. Fixed here; the check now reports them.

Consuming the `../` prefix is what makes them visible. Each repo-relative path
ends in a correct-looking `knowledge/<file>.md`, so a matcher that skips the
prefix resolves the suffix, finds the file, and passes the citation it should
reject — broadening the matcher without capturing the prefix would have hidden
these two a second way.

The fixture cited one file, as a Markdown link — the form the matcher already
handled, so it agreed with the blind spot rather than exposing it. It now cites
all three forms, and the assertion is the set found, not `length > 0`: a
narrowed matcher returns a subset whose every member resolves, which the old
assertion passed. Verified by restoring the link-only matcher: `alpha` found,
`beta` and `gamma` missed, test red.

Also pins `/bin/bash` at the one call site that reached it through PATH; the
other four in this file already did.

47 pass / 0 fail.
The `mobile-testing` skill reaches the knowledge file as
`../../knowledge/testing-layers.md`. That path resolves while browsing the
repo and 404s in the installed tree, which is the only tree a reader gets.
Each site already names the installed form beside it, so the source pointer
carries nothing the reader can follow.
@MajorLift
MajorLift force-pushed the jongsun/fix/knowledge-installs-once-per-domain branch from e317b17 to 004d5da Compare August 12, 2026 20:15
Absorbs #94. Knowledge is copied per domain, so a skill citing another
domain's file received nothing — four skills across coding, perps and
pr-workflow cite testing-layers.md, which lives in testing.
resolve_foreign_knowledge() delivers it.

The corpus test and the installer fix belong together: the test names
those four in KNOWN_UNRESOLVED, and leaving them in separate PRs meant
whichever landed second had to reconcile a list the other branch could
not see.

Its rationale is corrected here rather than the list emptied, because
the two are different properties. The corpus rule is a source-tree
convention — cite your own domain's knowledge, so the citation reads
without knowing the installer. Delivery is what the operator receives.
After this commit all four resolve for an operator and still break the
convention, and the old comment claimed they could never resolve for
any consumer or operator — an impossibility claim falsified by the same
change that carried it.

Falsifiable: stubbing resolve_foreign_knowledge fails both cross-domain
tests; restoring passes 21/21. Suite 74/74, lint 0 errors.
main added extension-testing (#130) with two repo-relative knowledge
citations — `../../knowledge/extension-testing-layers.md` and a
three-level variant — which are correct in this repo and 404 once
installed. Rewritten to the installed-relative form, the same fix this
branch already applied to mobile-testing.

This is the corpus check finding a real violation rather than a
regression: the branch was verified at 74/74 against a base six commits
stale, and CI ran it against a main where the violation existed.

Suite 74/74 merged with main; lint 52 skills, 0 errors.
@MajorLift MajorLift changed the title test(ci): Enforce that knowledge/ installations resolve fix(cli): deliver cross-domain knowledge, and enforce that citations resolve Aug 20, 2026
@MajorLift MajorLift changed the title fix(cli): deliver cross-domain knowledge, and enforce that citations resolve fix(cli): make every knowledge/ citation in an installed skill body resolve Aug 20, 2026
Absorbs #103. No file overlap with this branch and no conflict — the two
were adjacent in theme rather than entangled, unlike #94, which shared
test/cli.test.mjs and the same four citations.

Brings three reference checks in the linter: bare lane ids cited without
linking the catalog that defines them, [[snake_case]] wiki links from a
private authoring vault, and `## Related` entries naming a skill that
does not exist on the branch. Plus check-public-refs, which asserts every
repository named in the corpus resolves anonymously, and skill-audit.

Scope note for reviewers: the lane check has no corpus subject today.
Zero matches across 166 skill markdown files here, and zero across 256
non-evidence files on #84's branch, because evidence-catalog.md — the
only place [A-G]N vocabulary exists — lives on #84. It is fixture-tested
and prospective; it fires the day a non-evidence skill cites a lane.

Suite 84/84; lint 52 skills, 0 errors; check-public-refs 6/6 resolve.
Falsifiable: stubbing LANE_ID fails 2 lint tests, restoring passes 33/33.
@MajorLift MajorLift changed the title fix(cli): make every knowledge/ citation in an installed skill body resolve fix(cli): make skill references resolve — cross-domain knowledge delivery, and CI validation for the rest Aug 20, 2026
@MajorLift MajorLift changed the title fix(cli): make skill references resolve — cross-domain knowledge delivery, and CI validation for the rest fix(cli): make skill references resolve Aug 20, 2026
@MajorLift MajorLift changed the title fix(cli): make skill references resolve fix(cli): Ensure knowledge/ citations and cross-skill references resolve Aug 20, 2026
@MajorLift MajorLift changed the title fix(cli): Ensure knowledge/ citations and cross-skill references resolve fix(cli): make skill references resolve Aug 20, 2026
@MajorLift MajorLift changed the title fix(cli): make skill references resolve fix(cli): Ensure knowledge/ citations and cross-skill references resolve Aug 20, 2026
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