fix(packaging): move self-dev assets to top-level self/ (issue #138) - #203
Merged
Conversation
Claude Code has no plugin-payload exclusion mechanism (no .claudeignore,
no plugin.json files/exclude field) — confirmed empirically against a real
local plugin cache and by tracing the installer's copy path
(copyPluginToVersionedCache): the marketplace source directory is copied/
renamed into the cache wholesale, no filtering. Since the active
marketplace.json sources the plugin from `./.claude`, the only way to keep
reCode's self-development assets (self/checks.sh, self/gates.json,
self/pr-loop-self.md, self/smoke-fanout.sh, self/smoke/) out of a
consumer's plugin cache is to move them outside `.claude/` entirely.
Moves .claude/self/ -> top-level self/ and updates every reference:
gate.sh/checks.sh/smoke-fanout.sh path resolution, arm-loop.sh,
loop-*.sh, cockpit.sh, merge-ready.sh, worktree.sh, the *.test.sh suite,
.github/workflows/gates.yml's GATES_FILE, skills/setup/SKILL.md,
CONTRIBUTING.md, and docs/{GETTING_STARTED,HARDENING,MIGRATION,USAGE}.md.
site/ already lives at the repo root, outside .claude/, so it was already
excluded from the shipped payload under source "./.claude" — verified via
a git-tracked-content simulation of the packaging copy.
.claude/settings.json is left in place: it must stay at that exact path
for Claude Code to load it as this repo's own project settings during
self-hosted dev sessions, and it is provably inert for plugin consumers
(consumer-facing hook wiring ships separately via hooks/hooks.json using
${CLAUDE_PLUGIN_ROOT}); shipping it is harmless clutter, not a functional
leak.
Fixes #138
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e push restriction)
This PR's git-push credential is an OAuth App token without the `workflow`
scope, and GitHub refuses pushes that modify files under
.github/workflows/ from such tokens. Reverting this file back to main so
the branch can push at all.
Operator follow-up REQUIRED before/at merge — otherwise CI's `self-gates`
job will fail post-merge (it still points at .claude/self/gates.json,
which no longer exists after this PR moves it to self/gates.json). Apply
by hand (needs workflow-scoped push access):
self-gates:
env:
GATES_FILE: self/gates.json # was .claude/self/gates.json
(and the two comment lines a few lines above referencing the same old
path, cosmetic only).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…138) Review flagged a shared gap: nothing asserted the .claude/self/ -> self/ move stays permanent. do_build now fails if anything exists under .claude/self/, checking file existence only (not content/string refs, since .github/workflows/gates.yml still legitimately carries a dangling .claude/self/gates.json reference pending a workflow-scoped push).
robercano
approved these changes
Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
REQUIRED BEFORE/AT MERGE (owner — needs workflow-scoped push)
This session's git credential is an OAuth App token without GitHub's
workflowscope, so it cannotpush any change to
.github/workflows/*— GitHub rejects the push outright. That means.github/workflows/gates.ymlis unchanged frommainin this PR and still references the old,now-nonexistent
.claude/self/gates.jsonpath. Someone with workflow-scoped push access must apply thisexact diff before or at merge time:
Why not in this PR: the branch's push credential lacks the
workflowscope, so it cannot carry anyedit to a
.github/workflows/*file.Why load-bearing:
gate.shfails OPEN on a missing adapter (exits 0, "no gates.json found —skipping"), so leaving
GATES_FILEpointed at the now-deleted.claude/self/gates.jsonmakes theself-gatesCI job silently report green with zero real harness coverage on every future PR, untilthis one-line fix lands.
Problem
marketplace.jsonsources the plugin from./.claudeverbatim, so every consumer's plugin cacheshipped reCode's self-development assets (
self/checks.sh,self/gates.json,self/pr-loop-self.md,self/smoke-fanout.sh,self/smoke/) plus the inert.claude/settings.json. Scope was later extendedin an issue comment to also cover
site/(recode.thesolidchain.com), added after #138 was filed.Investigation: no non-moving exclusion mechanism exists
Checked, in order:
plugin.json's schema (nofiles/exclude/ignorefield — it only listscommandsexplicitly, and even that only scopes command auto-discovery, not payload copying), bothmarketplace.jsonfiles, the repo docs, and any.claudeignore-style convention (none). Then wentempirical:
(
~/.claude/plugins/cache/ai-project-orchestrator/orchestrator/0.2.1/) built from this exactmarketplace (
source: "./.claude"). It contains a byte-for-byte copy of.claude/self/*and.claude/settings.json— proof the installer does a verbatim recursive copy/rename of the wholesource directory, no filtering.
stringson the native binary — it'sa Bun-compiled bundle with readable minified source) and traced
copyPluginToVersionedCache: itresolves the checked-out source path and does
rename()/cpSync()of the entire directory, withno ignore-list, no per-file filter, no plugin.json-driven inclusion list beyond the
commandsallowlist (which only affects command auto-discovery, not what gets copied to disk).
Conclusion: no exclusion mechanism exists in the current plugin/marketplace format. Anything that
must never ship has to physically live outside the sourced directory (
.claude/).Approach chosen: move
self/out of.claude/(fallback (b))Moved
.claude/self/→ top-levelself/(sibling of.claude/,site/,docs/) and updated everyreference:
self/checks.shandself/smoke-fanout.shcomputed their repo root as two directories up(
.claude/self/checks.sh→../..); now one directory up (self/checks.sh→..).self/gates.json's own gate commands (bash .claude/self/checks.sh→bash self/checks.sh, etc.)and its
_README/module-description text..claude/scripts/{arm-loop,cockpit,gate,loop-census,loop-daemon,loop-event, merge-ready,worktree}.sh, the*.test.shsuite (cockpit,hooks-parity,loop-census,loop-event,needs-human,notify,plan-gate,pr-comment-fix,sync-managed-files),.claude/skills/setup/SKILL.md,.claude/.claude-plugin/README.md(rewrote the "why excluded"section — it's now structural, not allowlist-based),
CONTRIBUTING.md, anddocs/{GETTING_STARTED,HARDENING,MIGRATION,USAGE}.md..claude/scripts/merge-ready.test.sh's scenario G fixture: it uses.claude/self/gates.jsonas an arbitrary illustrative path to testGATES_FILE's relative-vs-absoluteresolution branches generically — unrelated to the real self-dev directory's location.
.claude/skills/setup/templates/arm-loop.sh(the MANAGED template) —the one line there referencing the old path is a cosmetic example in a
--gates-fileusage comment;bumping its version marker +
plugin.jsonversion for a non-functional string felt likedisproportionate collateral for this PR's scope, so it's left as a known, harmless follow-up
(flagged below) rather than triggering the managed-file version-bump machinery. (Re-confirmed on
review:
CONTRIBUTING.mdrequires bumping the marker +plugin.jsonversion for ANY change to amanaged template, so this stays deferred.)
Why not preference (a)
No non-moving packaging exclusion exists to prefer — see investigation above.
site/Already excluded, verified structurally:
site/lives at the repo root, a sibling of.claude/, andthe marketplace source is
./.claude— sosite/was never inside the copied tree to begin with.Confirmed with
git archive HEAD -- .claude, extracted to a scratch dir: nosite/anywhere in it(it isn't part of the archived pathspec at all, by construction).
.claude/settings.jsonKept as-is (not moved, not excluded) — deliberate call:
.claude/settings.jsonfor Claude Code to load it as this repo's ownproject settings during self-hosted dev sessions; moving it breaks live self-hosting.
the auto-discovered
hooks/hooks.json(using${CLAUDE_PLUGIN_ROOT}-relative commands); nothing ina consumer's plugin cache ever reads the cached
settings.jsonas if it were their own projectsettings.
it too — which (1) forbids. So: harmless, inert clutter, left in place.
Cache-cleanliness verification
Also cross-checked against the real, previously-installed local plugin cache mentioned above (before
this fix, it contained
self/*, confirming the bug existed) and against arsync-based simulation ofthe raw working tree (0.2.1 install layout matches the pre-fix bug precisely).
Structural regression check (added in review)
self/checks.sh'sdo_buildnow runsdo_packaging_exclusion: it FAILS the build if anything existsat
.claude/self/(file existence only — not a content/string grep, since.github/workflows/gates.ymlstill legitimately carries a dangling.claude/self/gates.jsonstringreference pending the workflow-scoped fix above, and a string-based assertion would wrongly fail on
that known exception). Verified both directions: passes on this branch as-is, and fails when a probe
file is placed under
.claude/self/.Gates (self adapter, since
.claude/**/docs/**route through it)Operator follow-up REQUIRED (live loop)
Separately from the CI workflow fix above, any live
pr-loop.service/claude-rc.servicesystemd unitsalready armed with
--gates-file .claude/self/gates.jsonneed re-arming with--gates-file self/gates.json(bash .claude/scripts/arm-loop.sh --gates-file self/gates.json) — thedaemon reads
GATES_FILEfrom its own unit'sEnvironment=at start, so an already-running instancewon't pick up the new path until re-armed.
Fixes #138