Skip to content

Stop vendoring the runtime harness into consumer repos (issue #134) - #156

Merged
robercano-ghbot merged 6 commits into
mainfrom
feat/issue-134-stop-vendoring
Jul 17, 2026
Merged

Stop vendoring the runtime harness into consumer repos (issue #134)#156
robercano-ghbot merged 6 commits into
mainfrom
feat/issue-134-stop-vendoring

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

Summary

Implements the owner's decision on #134 (option 1 — stop vendoring):

  • scaffold.sh / sync.sh no longer vendor agents/commands/hooks/scripts/skills into a consumer's local .claude/. Those now resolve at runtime via ${CLAUDE_PLUGIN_ROOT}, so the plugin cache stays the single source of truth (fixes the shadowing bug: resolve-roots.sh deliberately makes a repo-tracked .claude/scripts layout win over the plugin cache, which let a stale vendored copy permanently shadow a fresh plugin install — the reDeploy incident that prompted this issue). The .claude/.orchestrator-vendor marker and its copy machinery are removed. The 4 genuinely-managed files (feature-fanout.js, pr-loop.service, claude-rc.service, arm-loop.sh) are untouched by this change and keep working exactly as before.
  • /orchestrator:sync now DETECTS AND WARNS about local leftovers of the old vendored tree instead of silently deleting/restamping them: byte-identical to the plugin's shipped copy → flagged safe to delete; diverging → flagged conflict (possible deliberate local override, needs a human look). It never touches the directory itself. See detect_stale_vendor_copies in .claude/skills/sync/sync.sh.
  • pr-loop.service (v1 → v2): ExecStart now resolves loop-daemon.sh at unit-start time — prefers a repo-tracked .claude/scripts/loop-daemon.sh (self-hosting), else the newest ~/.claude/plugins/cache/*/orchestrator/*/scripts/loop-daemon.sh — instead of assuming a vendored repo-local copy.
  • .claude/settings.json template hooks now resolve via ${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/scripts/... instead of a hardcoded $CLAUDE_PROJECT_DIR/.claude/scripts/... path.
  • Docs (GETTING_STARTED.md, USAGE.md, MIGRATION.md, both setup/sync SKILL.md, MANIFEST.md) updated: the plugin must now stay enabled for everyday sessions (reversing the Headless sessions in plugin-consumer repos always die: session-start plugin load breaches remote-control's spawn-ack timeout #128-era "keep it disabled" guidance), and MIGRATION.md gets a short section for repos that onboarded during the Headless sessions in plugin-consumer repos always die: session-start plugin load breaches remote-control's spawn-ack timeout #128Consumer repos carry stale vendored .claude/scripts that shadow the fresh plugin cache — manage them or stop vendoring #134 vendored window.
  • Bumped plugin.json/marketplace.json to 0.2.2 + added a CHANGELOG.md entry, per the "bump the version on every real change" maintainer note (otherwise cached installs never pick this up).
  • Replaced vendor-runtime.test.sh (tested the removed copy machinery) with sync-managed-files.test.sh (37 checks: full MANAGED_FILES ladder + the new stale-vendor detect-and-warn behavior).
  • Unrelated pre-existing test bug fixed while gating this branch: loop-census.test.sh's main_dirty section reused a fixture that an earlier section left permanently dirty (an uncommitted pr-ci-fix.sh swap), tripping every main_dirty=no assertion regardless of which exclusion was under test. Committed the swap in the fixture so the assumption holds again.

Self-hosting safety

This repo's own .claude/self/ self-hosting is unaffected: resolve-roots.sh's repo-tracked-layout precedence is untouched (verified — no changes to resolve-roots.sh), and every script invocation across agents/commands/skills already used the ${CLAUDE_PLUGIN_ROOT:-<repo>/.claude} fallback idiom, which now also applies correctly to the settings.json hooks and pr-loop.service's resolver (both fall back to the repo-tracked copy when CLAUDE_PLUGIN_ROOT is unset, which is always true in this repo's own self-hosted sessions). .claude/self/gates.json-driven gates (build/lint/test) all pass in-worktree.

Migration caveat (from the issue)

A consumer's running pr-loop/claude-rc systemd unit holds its OLD script in memory until its unit restarts — refreshing/deleting files on disk is not enough. sync.sh's stale-vendor output and all updated docs explicitly tell the user to run systemctl --user restart pr-loop-<repo-slug>.service claude-rc-<repo-slug>.service after cleaning up a stale local copy (cf. the 2026-07-16 reCode deploy-lag incident, #131).

Gate results (.claude/self/gates.json, run from the worktree)

  • build: PASS
  • lint: PASS
  • test (checks.sh + smoke-fanout.sh): PASS

Deferred / follow-ups

  • pr-loop.service's plugin-cache discovery is a runtime glob (~/.claude/plugins/cache/*/orchestrator/*/scripts/loop-daemon.sh, newest wins) rather than an exact path baked in at ARM time — teaching arm-loop.sh to resolve+bake the precise plugin cache path via a new placeholder would be cleaner, but arm-loop.sh/claude-rc.service are off-limits to this PR (listed as separately-owned managed templates in the task scope). Flagging as a real follow-up, not a blocker — the glob fallback is tested manually and works against both the self-hosting and real-installed-plugin-cache cases on this machine.
  • The permission-allowlist entries in settings.json (Bash(bash .claude/scripts/gate.sh:*) etc.) are still relative-path prefixes; when ${CLAUDE_PLUGIN_ROOT} resolves to an absolute plugin-cache path, those prefixes won't match the literal command text, which could reintroduce permission prompts for plugin-enabled sessions. This is a pre-existing gap (agent prompts already used the ${CLAUDE_PLUGIN_ROOT:-.claude} idiom before this PR) — not introduced here, but now more likely to matter since the plugin must stay enabled. Worth a dedicated follow-up issue.

Fixes #134

🤖 Generated with Claude Code

robercano and others added 5 commits July 17, 2026 11:33
… (issue #134)

scaffold.sh/sync.sh no longer copy agents/commands/hooks/scripts/skills wholesale
into a consumer's local .claude/ (issue #128's model) — those resolve at runtime via
${CLAUDE_PLUGIN_ROOT} instead, so the plugin cache stays the single source of truth
and can't be permanently shadowed by a stale, unmanaged local copy (resolve-roots.sh
deliberately makes a repo-tracked .claude/scripts layout win, which is what let a
vendored copy silently outlive the plugin cache in reDeploy). The vendor-version
marker (.claude/.orchestrator-vendor) and its copy machinery are removed.

sync.sh replaces the old restamp/re-vendor ladder with detect_stale_vendor_copies:
it flags any local leftover of the old vendored tree (byte-identical to the plugin's
shipped copy -> safe to delete; diverging -> possible deliberate override, needs a
human look) but never deletes or restamps it — a legitimate local override must be
surfaced, not silently clobbered, per the issue's decision.

Replaces vendor-runtime.test.sh (tested the removed copy machinery) with
sync-managed-files.test.sh, covering the MANAGED_FILES ladder end to end plus the
new stale-vendor detect-and-warn behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ugin cache

pr-loop.service (v1 -> v2): ExecStart now resolves loop-daemon.sh at unit-start time
instead of assuming a vendored repo-local copy — prefers a repo-tracked
.claude/scripts/loop-daemon.sh (self-hosting), else the newest
~/.claude/plugins/cache/*/orchestrator/*/scripts/loop-daemon.sh. arm-loop.sh (the only
place that could bake an exact plugin-cache path via a new placeholder) is a
separately-owned managed template, so this is a runtime resolver rather than an
ARM-time substitution — a follow-up could teach arm-loop.sh to bake the exact path
instead.

settings.json template + consumer CLAUDE.md template: hook commands now resolve via
${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/scripts/... instead of a hardcoded
$CLAUDE_PROJECT_DIR/.claude/scripts/... path, and the plugin is now documented as
needing to stay ENABLED for everyday sessions (reversing the #128-era "keep it
disabled" guidance) since the runtime harness is no longer vendored locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion (issue #134)

Updates setup/sync SKILL.md, the setup MANIFEST, and docs/{GETTING_STARTED,USAGE,
MIGRATION}.md to describe the reverted vendoring model: the plugin must stay enabled
for everyday sessions, agents/commands/hooks/scripts/skills resolve via
${CLAUDE_PLUGIN_ROOT}, and /orchestrator:sync detects-and-warns about local leftovers
from an old vendored install instead of restamping/deleting them. Also documents the
migration caveat: a running pr-loop/claude-rc systemd unit holds its OLD script in
memory until its unit restarts, so cleaning up a stale local copy on disk isn't
enough on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…start clean

loop-census.test.sh's main_dirty section (issue #106) reuses fixture1 much later in
the file, assuming it's otherwise clean — but the earlier ci_fix_prs section
(issue #96) overwrites .claude/scripts/pr-ci-fix.sh in that same fixture without
committing the change, leaving it permanently tracked-but-modified for the rest of
the fixture's life. That untracked dirt isn't covered by any of the main_dirty
exclusions (sandbox-mask phantom paths, .claude/agents/, .claude/skills/setup/
templates/), so every main_dirty=no assertion after it failed regardless of which
exclusion was actually under test. Committing the swap in place restores the
fixture-clean assumption. Pre-existing gap, unrelated to issue #134 — found while
gating this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… stray marker without dirs (issue #134)

Review found sync.sh's stale-vendor detection flagged the plugin's own
agents/commands/hooks/scripts/skills as leftover vendor copies when run
against reCode itself (self_hosting=1), telling the operator to
reconcile/delete its own source tree and printing the systemd migration
caveat. Short-circuit detect_stale_vendor_copies to report "none found"
whenever plugin_root == target_root/.claude, and keep the call site
silent in that mode even if a stray legacy marker is present.

Also close a real gap: a consumer who deleted the vendored dirs by hand
but left a stray .claude/.orchestrator-vendor marker previously got no
warning at all, since the marker check only ran when a stale dir was
also found. It now warns about the leftover marker on its own (still
detect-only, never deletes), except in self-hosting mode.

Added scenarios 9 (self-hosting silence, with/without a stray marker)
and 10 (stray marker with no stale dirs warns) to
sync-managed-files.test.sh.
@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

Stop vendoring the runtime harness into consumer repos (issue #134) (not yet reviewed)

3 similar comments
@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

Stop vendoring the runtime harness into consumer repos (issue #134) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

Stop vendoring the runtime harness into consumer repos (issue #134) (not yet reviewed)

@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

Stop vendoring the runtime harness into consumer repos (issue #134) (not yet reviewed)

@robercano-ghbot
robercano-ghbot merged commit 9e2c988 into main Jul 17, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-134-stop-vendoring branch July 17, 2026 10:15
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.

Consumer repos carry stale vendored .claude/scripts that shadow the fresh plugin cache — manage them or stop vendoring

2 participants