Skip to content

fix(packaging): move self-dev assets to top-level self/ (issue #138) - #203

Merged
robercano-ghbot merged 3 commits into
mainfrom
feat/issue-138-packaging-exclude
Jul 30, 2026
Merged

fix(packaging): move self-dev assets to top-level self/ (issue #138)#203
robercano-ghbot merged 3 commits into
mainfrom
feat/issue-138-packaging-exclude

Conversation

@robercano-ghbot

@robercano-ghbot robercano-ghbot commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

REQUIRED BEFORE/AT MERGE (owner — needs workflow-scoped push)

This session's git credential is an OAuth App token without GitHub's workflow scope, so it cannot
push any change to .github/workflows/* — GitHub rejects the push outright. That means
.github/workflows/gates.yml is unchanged from main in this PR and still references the old,
now-nonexistent .claude/self/gates.json path. Someone with workflow-scoped push access must apply this
exact diff before or at merge time:

--- a/.github/workflows/gates.yml
+++ b/.github/workflows/gates.yml
@@ -45,8 +45,8 @@
   # This repo self-hosts on the orchestrator template: harness changes under
   # `.claude/**` and `docs/**` are exercised via the SELF adapter
-  # (.claude/self/gates.json), whose gates are actually implemented (node+bash
-  # checks in .claude/self/checks.sh). The `gates` job above reads the default,
+  # (self/gates.json), whose gates are actually implemented (node+bash
+  # checks in self/checks.sh). The `gates` job above reads the default,
   # placeholder adapter (.claude/gates.json), which has empty gate commands for
   # this repo and so skips cleanly — meaning harness/docs PRs would otherwise get
   # no real server-side check. This job closes that gap without touching the
@@ -64,6 +64,6 @@
         # Self-adapter gates are node+bash only, so the default toolchain
         # shipped on ubuntu-latest runners is sufficient — no setup action needed.
         env:
-          GATES_FILE: .claude/self/gates.json
+          GATES_FILE: self/gates.json
         run: bash .claude/scripts/gate.sh ${{ matrix.gate }}

Why not in this PR: the branch's push credential lacks the workflow scope, so it cannot carry any
edit to a .github/workflows/* file.
Why load-bearing: gate.sh fails OPEN on a missing adapter (exits 0, "no gates.json found —
skipping"), so leaving GATES_FILE pointed at the now-deleted .claude/self/gates.json makes the
self-gates CI job silently report green with zero real harness coverage on every future PR, until
this one-line fix lands.


Problem

marketplace.json sources the plugin from ./.claude verbatim, so every consumer's plugin cache
shipped 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 extended
in 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 (no files/exclude/ignore field — it only lists
commands explicitly, and even that only scopes command auto-discovery, not payload copying), both
marketplace.json files, the repo docs, and any .claudeignore-style convention (none). Then went
empirical:

  • Found a real, already-installed local plugin cache on this machine
    (~/.claude/plugins/cache/ai-project-orchestrator/orchestrator/0.2.1/) built from this exact
    marketplace (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 whole
    source directory
    , no filtering.
  • Extracted the bundled Claude Code binary's embedded JS bundle (strings on the native binary — it's
    a Bun-compiled bundle with readable minified source) and traced copyPluginToVersionedCache: it
    resolves the checked-out source path and does rename()/cpSync() of the entire directory, with
    no ignore-list, no per-file filter, no plugin.json-driven inclusion list beyond the commands
    allowlist (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-level self/ (sibling of .claude/, site/, docs/) and updated every
reference:

  • Path-resolution fixes inside the moved files themselves: self/checks.sh and
    self/smoke-fanout.sh computed 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.shbash self/checks.sh, etc.)
    and its _README/module-description text.
  • Every other reference: .claude/scripts/{arm-loop,cockpit,gate,loop-census,loop-daemon,loop-event, merge-ready,worktree}.sh, the *.test.sh suite (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, and
    docs/{GETTING_STARTED,HARDENING,MIGRATION,USAGE}.md.
  • Deliberately did not touch .claude/scripts/merge-ready.test.sh's scenario G fixture: it uses
    .claude/self/gates.json as an arbitrary illustrative path to test GATES_FILE's relative-vs-absolute
    resolution branches generically — unrelated to the real self-dev directory's location.
  • Deliberately did not bump .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-file usage comment;
    bumping its version marker + plugin.json version for a non-functional string felt like
    disproportionate 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.md requires bumping the marker + plugin.json version for ANY change to a
    managed 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/, and
the marketplace source is ./.claude — so site/ was never inside the copied tree to begin with.
Confirmed with git archive HEAD -- .claude, extracted to a scratch dir: no site/ anywhere in it
(it isn't part of the archived pathspec at all, by construction).

.claude/settings.json

Kept as-is (not moved, not excluded) — deliberate call:

  1. It must stay at exactly .claude/settings.json for Claude Code to load it as this repo's own
    project settings during self-hosted dev sessions; moving it breaks live self-hosting.
  2. It is provably inert for plugin consumers: consumer-facing hook wiring already ships separately via
    the auto-discovered hooks/hooks.json (using ${CLAUDE_PLUGIN_ROOT}-relative commands); nothing in
    a consumer's plugin cache ever reads the cached settings.json as if it were their own project
    settings.
  3. With no exclusion mechanism (see above), the only way to keep it out of the cache would be to move
    it too — which (1) forbids. So: harmless, inert clutter, left in place.

Cache-cleanliness verification

git archive HEAD -- .claude | tar -x   # tracked-content-only, matches the real installer's git copy
find <extracted>/.claude -iname "*self*"   # → empty
find <extracted>/.claude -maxdepth 1        # → .claude-plugin, agents, commands, gates.json, hooks,
                                             #   scripts, settings.json, skills, systemd, workflows
                                             #   (no self/, no site/)

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 a rsync-based simulation of
the raw working tree (0.2.1 install layout matches the pre-fix bug precisely).

Structural regression check (added in review)

self/checks.sh's do_build now runs do_packaging_exclusion: it FAILS the build if anything exists
at .claude/self/ (file existence only — not a content/string grep, since
.github/workflows/gates.yml still legitimately carries a dangling .claude/self/gates.json string
reference 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)

GATES_FILE=self/gates.json bash .claude/scripts/gate.sh build   # ✓ passed (incl. new packaging-exclusion check)
GATES_FILE=self/gates.json bash .claude/scripts/gate.sh lint    # ✓ passed
GATES_FILE=self/gates.json bash .claude/scripts/gate.sh test    # ✓ passed (incl. self/smoke-fanout.sh)

Operator follow-up REQUIRED (live loop)

Separately from the CI workflow fix above, any live pr-loop.service/claude-rc.service systemd units
already armed with --gates-file .claude/self/gates.json need re-arming with
--gates-file self/gates.json (bash .claude/scripts/arm-loop.sh --gates-file self/gates.json) — the
daemon reads GATES_FILE from its own unit's Environment= at start, so an already-running instance
won't pick up the new path until re-armed.

Fixes #138

robercano and others added 2 commits July 30, 2026 11:53
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 <[email protected]>
…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 <[email protected]>
…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-ghbot
robercano-ghbot merged commit bb815b6 into main Jul 30, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-138-packaging-exclude branch July 30, 2026 10:18
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.

Packaging: exclude self-development assets (.claude/self/) from the shipped plugin

2 participants