maniator / maniator/verticopolis
[Feature]: Clear out set of .claude agent files and replace with symlink
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 5
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
Problem / motivation
The /.claude and /.agent are duplicative files due to claude only wanting to use that file name for its goodies. However it will follow a symlink. Various checks can enforce the behavior. However trickiness ensues if a windows machine running git has not been told to allow symlink.
This is WIP but i figured I should make it available tonight. This does not include removing the skills that are not used in the platform (eg roblox, unreal etc) and other machinery for generating games that we are not involved with. (I also saw at least one report generated in this process saying that copilot maxes out at 500 skill files and there is somewhat more than that, so we should try to determine if copilot is getting maxed out by skill files.)
Related refactors:
- #825 move screenshots to another repo
- #820 simplify agentic frameworks
- The WIP of code around this idea so far #828
Proposed solution
Use a symlink to replace the .claude files. There is additionally a package.json to deal with.
The edge case hunter layer ran on this stuff and gave me the following.
Skill-library symlink change: edge-case findings
Status: WIP. Findings only, nothing triaged, nothing fixed.
Scope reviewed: the uncommitted work on feat/debug-surface that replaces the
duplicated .claude/skills/ tree with a committed symlink to .agents/skills/,
plus the new guard test, the doc updates, and the Vercel ignore-build change.
Reproduce the non-deletion diff with:
git diff HEAD -- . ':(exclude).claude/skills/*'
How this was produced: the bmad-review-edge-case-hunter skill, run read-only.
This is one of three layers. Per CLAUDE.md the deep review is Blind Hunter,
then Edge Case Hunter, then Acceptance Auditor, then triage. This change is
storage / tooling / plumbing, so the skill to run is /bmad-code-review, not
/gds-code-review. Running this layer alone does not satisfy the non-negotiable.
Not yet done: patch vs defer triage, backlog rows, GH issue numbers.
Rough first read: items 1-4 look like patch (the guard is the point of the
change, and today it has two ways to pass while broken and two ways to fail while
fine), 5-9 are judgment calls, 10-14 are cheap doc fixes probably worth just
doing.
P1: the guard is wrong or absent when it matters
1. Guard silently disappears when git errors
src/tests/agentSkillsSingleCopy.guard.test.ts:26-31,43
The catch is unconditional, so it swallows every failure mode identically:
safe.directory "dubious ownership" (any Docker or podman run over a
bind-mounted host checkout), a corrupt index, a permissions failure. Each returns
null, and describe.skipIf(index === null) then skips all three tests. Nothing
anywhere asserts that the guard actually ran.
Consequence: the single-copy invariant can go unenforced indefinitely with a
fully green npm test.
2. Empty or non-matching index fails hard instead of skipping
src/tests/agentSkillsSingleCopy.guard.test.ts:34-46
git ls-files -s -- .claude .agents/skills exits 0 with empty stdout whenever
the pathspec matches nothing. Verified empirically in four shapes: a fresh
git init, a source tarball that was later git inited, this tree vendored as a
plain subdirectory of another repo, and a sparse checkout that excluded these
cones.
index is then "", not null, so skipIf does not fire, claudeEntries is
[], and expect([]).toEqual([".claude/skills"]) fails. The docblock at lines
27-30 explicitly promises exactly these cases skip ("a source tarball, a vendored
copy. Nothing to guard in that case"). They do not.
3. Quoted paths hide the duplicate the guard exists to catch
src/tests/agentSkillsSingleCopy.guard.test.ts:45,56
With core.quotePath at its default, git C-quotes any path containing non-ASCII
or control characters. Verified output:
100644 c1b0730e... 0 ".agents/skills/caf\303\251.md"
A re-added .claude/skills/... file with such a name is emitted with a leading
", so e.path.startsWith(".claude/") is false and the entry is dropped from
claudeEntries.
Consequence: the assertion passes while the invariant it names is violated.
Fix direction: ls-files -sz and split on NUL, which git never quotes.
4. The real failure mode has no detector at all
src/tests/agentSkillsSingleCopy.guard.test.ts:17-20,43-58
Every assertion reads git's index. On a core.symlinks=false checkout (Windows
without Developer Mode, some CI images, git archive extractions),
.claude/skills lands as a 17-byte text file, Claude Code discovers zero
project skills, and git reports the tree clean because the recorded index mode is
still 120000. All three tests pass.
The same gap covers the docblock's own motivating scenario while a materialized
tree is still unstaged. Prose in AGENTS.md is the only mitigation that exists.
P2: false failures and missed coverage
5. Any other tracked file under .claude/ fails the test though the invariant holds
src/tests/agentSkillsSingleCopy.guard.test.ts:45-46
toEqual([".claude/skills"]) is an exact-list assertion over everything under
.claude/. Committing .claude/settings.json (the standard shared
project-settings file, and what the update-config skill writes),
.claude/agents/*.md, .claude/commands/*.md, or a .claude/hooks/ script
fails a test named "BMAD/BMGD skills are committed exactly once" even though the
skill library is still committed exactly once.
Only .claude/settings.local.json is ignored today, and only via the user's
personal global gitignore (~/.gitignore_global:101), not the repo's. So
nothing in-repo prevents the collision for another contributor.
6. Merge-conflict stages break both assertions with misleading messages
src/tests/agentSkillsSingleCopy.guard.test.ts:46,51
During an unresolved conflict on .claude/skills, which is exactly what a
Windows-authored branch merging into main produces, ls-files -s emits stages 1,
2, and 3. claudeEntries becomes three identical .claude/skills paths and
toEqual fails. Separately, git cat-file -p :.claude/skills reads stage 0,
which does not exist during a conflict, so it errors and blob?.trim() is
undefined.
Consequence: two confusing failures whose messages point at the wrong problem, at
the moment a reviewer most needs a clear signal.
7. Only two paths are inspected, so a third copy passes
src/tests/agentSkillsSingleCopy.guard.test.ts:34,55-58
The pathspec covers .claude and .agents/skills. Re-adding the library at
.github/skills/, .agents/skills-backup/, _bmad/skills/, or anywhere else
satisfies all three assertions. The test title claims "committed exactly once";
the assertions establish only "not under .claude/".
8. toBeGreaterThan(100) is loose in one direction and brittle in the other
src/tests/agentSkillsSingleCopy.guard.test.ts:57
The real tree is 515 files, so the threshold still passes at 101 and an
accidental deletion of ~80% of the library goes uncaught. In the other direction,
the trim described in skills-trim-plan.md targets a much smaller library, and
if it lands under 100 files this guard fails for a change it was never meant to
police.
9. :(exclude).agents is broader than the intent
scripts/vercel-ignore-build.sh:43
The pathspec excludes the whole .agents tree, not .agents/skills. The
script's own header at lines 12-18 documents the denylist as safe because "a
forgotten entry costs only a harmless extra build, never a missed deploy". A
too-broad entry inverts that property: the first build input placed anywhere
under .agents/ becomes a silent missed deploy, with no failing check anywhere.
The header at lines 12-13 also still enumerates the excluded set as "docs,
skills, bmad artifacts, workflows, e2e, and the root doc markdown" without naming
.agents, so the next person auditing the comment against the pathspecs finds a
mismatch.
P3: docs now assert things that are unverified or untrue
10. The Copilot loader claim is load-bearing but unverified
.github/copilot-instructions.md:115-121
"The file count per convention is unchanged, so this note still applies and the
loader still drops the skills" holds only if Copilot's skill materializer follows
the .claude/skills symlink. If it does not, the convention now counts 1
entry, the ~508-file abort stops firing, and the note's conclusion ("a
.github/skills/code-review skill can't load here ... don't re-add a review skill
expecting it to load") becomes false while still instructing maintainers not to
try. The run log quoted at line 104 is from the pre-symlink layout and is no
longer evidence for the current state.
This one needs an empirical check nobody could run in-session: look at a real
Copilot review run log from after the change and see what file count it reports
for the .claude/skills convention.
11. "Tracked separately" points at nothing in the repo
.github/copilot-instructions.md:120-121
"Getting under the budget needs the library itself trimmed, which is tracked
separately." There is no row for this in
_bmad-output/implementation-artifacts/backlog.md and no GH issue, and
skills-trim-plan.md is untracked, so it is not in the repo the doc describes.
CLAUDE.md's standing rule: a doc may claim something is tracked only when a
real row exists.
12. Windows guidance covers only fresh clones
AGENTS.md:34-36
The guidance gives git clone -c core.symlinks=true. A contributor who already
cloned has no path forward stated: setting core.symlinks=true alone does not
rewrite the already-checked-out file, and git reports no diff, so the tree looks
clean while /bmad-* and /gds-* silently do not exist. The recovery
(git config core.symlinks true, then git rm --cached .claude/skills and
git checkout -- .claude/skills, or delete and re-checkout) is not mentioned.
13. git archive and "Download ZIP" lose the skills
Zip export writes symlinks as regular files containing the target string unless
the reader honors the symlink attribute, and there is no .gitattributes in the
repo to change that. Before this change, a ZIP download carried a working
.claude/skills/ tree. Now it carries a 17-byte text file, with no error.
14. streamlined-process.md now states falsehoods
Lines 29-30, 119, and 131. It instructs
diff -rq .agents/skills .claude/skills && echo IDENTICAL and
git ls-files .claude | wc -l as the duplicate check, and asserts at line 119
that the two trees are byte-identical duplicates. That count now returns 1. The
file is untracked so this change does not ship it, but it reads as current
guidance to anyone who opens it.
Verified handled, deliberately not reported
Listed so a later pass does not re-derive them.
- No repo tooling traverses the symlink.
npm run lintiseslint src api;
tsconfig.jsonincludeissrc/**/*.ts; both vitest project includes are
src/**; everyreaddirSyncwalker insrc/tests/andscripts/is rooted at
src,scripts, ordist. No double-traversal, no cycle. There are also no
.jsor.tsfiles anywhere under.agents/skills(census: 395 md, 80 toml,
14 csv, 11 py, 6 html, 5 yaml, 4 json). - Every vercel-ignore-build commit shape resolves correctly, including the
rename-shaped delete of.claude/skills/**plus add of the symlink (both fall
under:(exclude).claude, so skip), mixed commits (build via the non-excluded
paths), and the first-deploy and shallow-clone paths (guarded at lines 27-34). pr-drift-check.yml's render filter correctly ignores all 515
.claude/skills/**deletions; none of itscasepatterns match.- No em-dashes in any added prose, and the unbumped
package.jsonversion
(2.22.0) is correct for an internal-only change with no player-facing effect. - The test file's placement in
src/tests/matches existing guard-test
precedent (fileSize.guard.test.ts,iconCoverage.guard.test.ts,
backlogIssueMirror.test.ts) and is excluded from coverage by the
src/tests/**glob, so it does not move the floors. - Environment boundaries that work: git worktrees, submodules, shallow
clones, detached HEAD,actions/checkout's default depth-1, repo paths
containing spaces (execFileSyncruns without a shell), and git writing to
stderr (stdio[2]is"ignore").describe.skipIfalso evaluates at module
load, before any test body, which is the correct time.
Alternatives considered
No response
Which game mode should this target?
Both / mode-agnostic
Before submitting
- I searched existing issues and this is not a duplicate.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading src/tests/agentSkillsSingleCopy.guard.test.ts, scripts/vercel-ignore-build.sh, AGENTS.md, and .github/copilot-instructions.md to understand the proposed symlink change and the reported edge cases. Because the issue is explicitly WIP, done requires a maintainer-approved scope, resolved triage for the findings, and tests and documentation that accurately cover the supported checkout and tooling behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, typescript
- Domain
- build-system, developer-experience, documentation, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100