From b67d9f288f0ef71b02c6ef229bf596fc7e25883a Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 26 Oct 2026 03:00:25 +0000 Subject: [PATCH] fix(context-bill): a root container skips a nested skill reached through a directory link at any depth Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/context-bill.ts | 17 ++++++++++++----- test/context-bill.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/context-bill.ts b/lib/context-bill.ts index 716bf6781..e0552c787 100644 --- a/lib/context-bill.ts +++ b/lib/context-bill.ts @@ -284,7 +284,11 @@ export interface WalkedFile { * * With `skipNestedSkills`, a top-level subdirectory holding its own SKILL.md is * not entered, so a container skill keeps the files it shares with a nested - * skill instead of losing them to whichever path sorted first. + * skill instead of losing them to whichever path sorted first. The root + * container also skips a nested skill reached through a directory link at any + * depth: discovery bills that skill on its own, and the plain walk this + * replaced never followed directory links, so counting it here too would bill + * it twice. * * Every directory is read through its realpath, so a lookup crosses at most * one link however deep the lexical path runs: a chain of links never hits @@ -301,8 +305,9 @@ export function walkMdFiles(dir: string, opts: { billedRoot?: string; skipNested return out; } const seen = new Set([rootReal]); + const container = rootReal === billedReal; let entryCount = 0; - const walk = (current: string, currentReal: string) => { + const walk = (current: string, currentReal: string, viaLink: boolean) => { let entries: fs.Dirent[]; try { entries = fs.readdirSync(currentReal, { withFileTypes: true }); @@ -320,7 +325,9 @@ export function walkMdFiles(dir: string, opts: { billedRoot?: string; skipNested } else if ( kind !== "dir" || fs.existsSync(path.join(viaReal, ".git")) || - (opts.skipNestedSkills && current === dir && isFile(path.join(viaReal, "SKILL.md"))) + (opts.skipNestedSkills && + (current === dir || (container && (viaLink || e.isSymbolicLink()))) && + isFile(path.join(viaReal, "SKILL.md"))) ) { continue; } @@ -347,10 +354,10 @@ export function walkMdFiles(dir: string, opts: { billedRoot?: string; skipNested } if (encloses(real, rootReal) || encloses(real, billedReal)) continue; seen.add(real); - walk(p, real); + walk(p, real, viaLink || e.isSymbolicLink()); } }; - walk(dir, rootReal); + walk(dir, rootReal, false); return out; } diff --git a/test/context-bill.test.ts b/test/context-bill.test.ts index c82156295..12b9f0ba2 100644 --- a/test/context-bill.test.ts +++ b/test/context-bill.test.ts @@ -398,6 +398,32 @@ describe("upstream fix b: installed-tree layout (repo-checkout subdir skip)", () fs.rmSync(root, { recursive: true, force: true }); }); + it("keeps a root container from billing a nested skill that a deeper directory link reaches", () => { + if (process.platform === "win32") return; + const root = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-container-deep-")); + fs.writeFileSync(path.join(root, "SKILL.md"), "---\nname: c\ndescription: d\n---\n# c\n"); + fs.mkdirSync(path.join(root, "sub")); + fs.writeFileSync(path.join(root, "sub", "SKILL.md"), "---\nname: s\ndescription: d\n---\n# s\n"); + fs.writeFileSync(path.join(root, "sub", "big.md"), "b".repeat(10_000)); + fs.mkdirSync(path.join(root, "refs")); + fs.writeFileSync(path.join(root, "refs", "note.md"), "n"); + fs.symlinkSync(path.join(root, "sub"), path.join(root, "refs", "alias")); + const outside = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-container-outside-")); + fs.mkdirSync(path.join(outside, "deep")); + fs.writeFileSync(path.join(outside, "deep", "SKILL.md"), "---\nname: o\ndescription: d\n---\n# o\n"); + fs.writeFileSync(path.join(outside, "deep", "o.md"), "o".repeat(5_000)); + fs.mkdirSync(path.join(root, "linked")); + fs.symlinkSync(outside, path.join(root, "linked", "via")); + + const bill = buildBill(root); + const byName = Object.fromEntries(bill.skills.map((s) => [s.name, s])); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(outside, { recursive: true, force: true }); + expect(Object.keys(byName).sort()).toEqual([path.basename(root), path.join("linked", "via", "deep"), path.join("refs", "alias"), "sub"].sort()); + expect(byName[path.basename(root)].totalMdBytes).toBe("---\nname: c\ndescription: d\n---\n# c\n".length + 1); + expect(byName[path.join("refs", "alias")].totalMdBytes).toBe(byName.sub.totalMdBytes); + }); + it("walks a chain of links deeper than the kernel's link limit without losing files", () => { if (process.platform === "win32") return; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "context-bill-deep-chain-"));