Skip to content

infra(symlinks): #357 sub-task 4 — Windows symlink materialization gate#364

Merged
silongtan merged 2 commits into
devfrom
infra/357-subtask-4-windows-symlink
May 15, 2026
Merged

infra(symlinks): #357 sub-task 4 — Windows symlink materialization gate#364
silongtan merged 2 commits into
devfrom
infra/357-subtask-4-windows-symlink

Conversation

@silongtan

Copy link
Copy Markdown
Collaborator

Summary

Implements #357 sub-task 4 (the last sub-task on this tracking issue). PR #307 replaced .claude/skills/bicameral-* duplicates with symlinks but shipped no enforcement that Windows clones materialize them correctly. This PR closes that loop.

Stacks logically alongside the rest of the #357 cluster (#359 merged, #360, #361) but branches independently from dev — can merge in any order.

Why

Git stores the 22 .claude/skills/bicameral-* entries as mode-120000 symlinks. Windows defaults to core.symlinks=false and silently materializes them as plain text files containing the target path string ("../../skills/bicameral-preflight"). The MCP test surface still passes because nothing tries to follow the symlinks; the breakage surfaces only when Claude Code's slash-command resolver tries to resolve /bicameral-preflight. PR #307's body noted this in passing but added no gate, so it remained the "verified in-vivo" claim Devin's #357 critique correctly flagged.

Three defense-in-depth layers

1. tests/test_skills_symlink_integrity.py (NEW)

Two assertions, both with actionable failure messages:

  • test_skills_symlinks_tracked_as_mode_120000git ls-files -s shows ≥ 22 mode-120000 entries under .claude/skills/. Catches the regression where a symlink got re-committed as a plain file (whether by Windows-clone misconfiguration or by a misguided IDE auto-fix).

  • test_skills_symlinks_materialize_on_this_clone — on-disk .claude/skills/bicameral-preflight must resolve as a real symlink. If it's a plain file containing "../../skills/...", surfaces the specific fix commands inline:

    git config --global core.symlinks true
    git rm --cached .claude/skills/bicameral-*
    git checkout -- .claude/skills/
    

    Contributors never have to grep for "how do I fix this" when CI tells them outright.

2. .github/workflows/test-mcp-regression.yml

Two new steps run BEFORE the regular test suite on both ubuntu-latest and windows-latest:

  • Asserts the mode-120000 count via git ls-files
  • Asserts on-disk materialization via a Python probe with ::error:: annotations

The windows-latest matrix slot is where this actually matters — ubuntu-latest always materializes symlinks correctly. Now the matrix isn't just running tests on Windows; it's gating the symlink contract there too.

3. CLAUDE.md

Upgraded the Windows note from "recommended" framing to "required", with the specific fix-in-place commands AND a pointer to the test + workflow gate that enforces the contract.

Why NOT in setup_wizard.py

The wizard's _install_skills path is for wheel installs — bundled skills/ content is copied into the target repo's .claude/skills/. There are no symlinks at that layer; nothing to check. The Windows symlink issue only affects developer clones of this repo itself, where the test + CI gates are the right surface.

Verification

$ pytest tests/test_skills_symlink_integrity.py -v
tests/test_skills_symlink_integrity.py::test_skills_symlinks_tracked_as_mode_120000 PASSED
tests/test_skills_symlink_integrity.py::test_skills_symlinks_materialize_on_this_clone PASSED
============================== 2 passed in 0.16s ==============================

Negative-case verification (manually edited test to assert count > 1000): fails with the actionable error message listing the fix command.

Test plan

  • CI green on ubuntu-latest (the new steps pass cleanly)
  • CI green on windows-latest — the more interesting check, since this is where the gate actually trips on misconfigured clones
  • After merge, intentionally check out the repo with core.symlinks=false on a Windows VM, run pytest, confirm the gate fires with the documented fix

What's left on #357

With this PR, all four acceptance criteria of #357 have a delivered PR:

The remaining work on #357 itself is the 8 trap-row backfill PRs that the sub-task 1 audit identified — sequenced after sub-tasks 2/3/4 land.

🤖 Generated with Claude Code

PR #307 replaced .claude/skills/bicameral-* duplicates with symlinks to
canonical skills/bicameral-*. Git stores those as mode-120000 entries
on every platform, but Windows defaults to core.symlinks=false and
materializes them as plain text files containing the target path string
— the symlink-as-string failure mode that breaks slash-command resolution
silently. PR #307 noted this in passing but shipped no enforcement.
This commit closes the loop.

Three layers of enforcement (all defense-in-depth):

1. `tests/test_skills_symlink_integrity.py` (NEW) — pytest-level gate.
   Two assertions:
   - test_skills_symlinks_tracked_as_mode_120000: git ls-files shows
     at least 22 mode-120000 entries under .claude/skills/. Catches
     regressions where a symlink got re-committed as a plain file.
   - test_skills_symlinks_materialize_on_this_clone: the on-disk
     .claude/skills/bicameral-preflight resolves as a symlink (or, on
     Windows with core.symlinks=false, contains the path string
     '../../skills/...' which triggers a loud failure with the specific
     fix commands).
   Runs on every `pytest tests/` invocation in this repo — Windows
   contributors see the failure with the fix command before they push.

2. `.github/workflows/test-mcp-regression.yml` — CI gate at the workflow
   level. Two new steps run BEFORE the regular test suite:
   - Asserts the mode-120000 entry count in git ls-files
   - Asserts the on-disk materialization via a Python probe
   Runs on both ubuntu-latest and windows-latest via the existing
   matrix.os. The windows-latest run is where this actually matters —
   ubuntu always materializes symlinks correctly.

3. `CLAUDE.md` — Windows note upgraded from "recommended" to "required",
   with the specific fix-in-place commands (`git rm --cached` + `git
   checkout`) for contributors who already cloned without core.symlinks
   set. References the new test + workflow gate so the contract is
   self-documenting.

Why NOT in setup_wizard.py: the wizard path is for *wheel installs*,
where bundled skills/ content gets COPIED into the target repo's
.claude/skills/. There are no symlinks at that layer — nothing to
check. The Windows symlink issue only affects developer clones of
this repo itself, where the test + CI gates are the right surface.

Verified locally: 2/2 tests pass against the existing macOS clone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@silongtan silongtan added flow:feature Standard feature/fix PR targeting BicameralAI/dev (the default flow) P2 Medium: next milestone or two; default for new issues post-triage infra Infrastructure / build / CI / repo-admin work test Test infrastructure, fixtures, or coverage work labels May 15, 2026
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1947dab6-f0e1-4ca9-ac7f-c848b05acd4c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch infra/357-subtask-4-windows-symlink

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The sub-task 4 workflow step's inline Python script printed em-dash and
right-arrow characters. Windows Python defaults to cp1252 for stdout,
which can't encode either — UnicodeEncodeError crashed the step right
after a successful symlink check. The check itself worked; the diagnostic
print is what failed.

Replaces em-dashes with hyphens and arrows with '->'. Could also have
set PYTHONIOENCODING=utf-8 but ASCII keeps the change scope narrower.

Failure log on PR #364:
  UnicodeEncodeError: 'charmap' codec can't encode character '→'
  in position 62: character maps to <undefined>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@silongtan silongtan merged commit 24d70db into dev May 15, 2026
8 checks passed
@silongtan silongtan deleted the infra/357-subtask-4-windows-symlink branch May 15, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flow:feature Standard feature/fix PR targeting BicameralAI/dev (the default flow) infra Infrastructure / build / CI / repo-admin work P2 Medium: next milestone or two; default for new issues post-triage test Test infrastructure, fixtures, or coverage work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant