From 1dd62170ffbf45e8fb6b69f30a82674a66ff0127 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:53:24 +0200 Subject: [PATCH 1/3] fix(packaging): move self-dev assets to top-level self/ (issue #138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/.claude-plugin/README.md | 18 ++++++---- .claude/gates.json | 2 +- .claude/scripts/arm-loop.sh | 2 +- .claude/scripts/cockpit.sh | 2 +- .claude/scripts/cockpit.test.sh | 10 +++--- .claude/scripts/gate.sh | 4 +-- .claude/scripts/hooks-parity.test.sh | 4 +-- .claude/scripts/loop-census.sh | 2 +- .claude/scripts/loop-census.test.sh | 2 +- .claude/scripts/loop-daemon.sh | 2 +- .claude/scripts/loop-event.sh | 4 +-- .claude/scripts/loop-event.test.sh | 4 +-- .claude/scripts/merge-ready.sh | 2 +- .claude/scripts/needs-human.test.sh | 4 +-- .claude/scripts/notify.test.sh | 4 +-- .claude/scripts/plan-gate.test.sh | 2 +- .claude/scripts/pr-comment-fix.test.sh | 2 +- .claude/scripts/sync-managed-files.test.sh | 4 +-- .claude/scripts/worktree.sh | 2 +- .claude/skills/setup/SKILL.md | 2 +- .github/workflows/gates.yml | 6 ++-- CONTRIBUTING.md | 18 +++++----- docs/GETTING_STARTED.md | 2 +- docs/HARDENING.md | 2 +- docs/MIGRATION.md | 4 +-- docs/USAGE.md | 10 +++--- examples/README.md | 2 +- examples/fixture-target/README.md | 4 +-- examples/fixture-target/gates.json | 2 +- {.claude/self => self}/README.md | 33 +++++++++++-------- {.claude/self => self}/checks.sh | 12 +++---- {.claude/self => self}/gates.json | 14 ++++---- {.claude/self => self}/pr-loop-self.md | 16 ++++----- {.claude/self => self}/smoke-fanout.sh | 6 ++-- {.claude/self => self}/smoke/broken.patch | 0 .../self => self}/smoke/implementer.patch | 0 36 files changed, 109 insertions(+), 100 deletions(-) rename {.claude/self => self}/README.md (67%) rename {.claude/self => self}/checks.sh (93%) rename {.claude/self => self}/gates.json (84%) rename {.claude/self => self}/pr-loop-self.md (64%) rename {.claude/self => self}/smoke-fanout.sh (97%) rename {.claude/self => self}/smoke/broken.patch (100%) rename {.claude/self => self}/smoke/implementer.patch (100%) diff --git a/.claude/.claude-plugin/README.md b/.claude/.claude-plugin/README.md index 02a9d16..d0bdede 100644 --- a/.claude/.claude-plugin/README.md +++ b/.claude/.claude-plugin/README.md @@ -12,10 +12,11 @@ that ship to downstream installs are what runs the live self-hosted PR loop here `/orchestrator:harden`, etc. No file renames are needed for this — the namespace comes from `name` in `plugin.json`, not from filenames. - `plugin.json` carries an explicit `commands` allowlist (issue #76) so only consumer-facing - commands ship downstream. `.claude/self/pr-loop-self.md` — this repo's own self-hosting loop - prompt — is deliberately excluded: it lives under `.claude/self/` (not `.claude/commands/`), so - it is never auto-discovered as a project slash command either. See `.claude/self/README.md` for - how to run it in-repo. + commands ship downstream. `self/pr-loop-self.md` — this repo's own self-hosting loop + prompt — is deliberately excluded: it lives at the top-level `self/` (outside `.claude/` + entirely, issue #138), not `.claude/commands/`, so it is never auto-discovered as a project + slash command either — nor copied into the plugin payload at all, since the marketplace + source is `./.claude`. See `self/README.md` for how to run it in-repo. ## The `${CLAUDE_PLUGIN_ROOT:-.claude}` fallback Agent/command prompts invoke scripts as: @@ -35,9 +36,12 @@ consumer-facing command files — this disables the default directory-wide auto- Not distributed by this plugin (repo-scaffolded, project-specific): - `.claude/workflows/*.js` — deterministic fan-out workflows, not plugin-portable. -- `.claude/self/*` — this repo's OWN self-hosting adapter (gates, checks, and the - `pr-loop-self.md` loop prompt), not for downstream projects; downstream adopters get the - placeholder `.claude/gates.json` instead. +- top-level `self/*` (issue #138) — this repo's OWN self-hosting adapter (gates, checks, and + the `pr-loop-self.md` loop prompt), not for downstream projects; downstream adopters get the + placeholder `.claude/gates.json` instead. Unlike the other exclusions above, this one is + structural rather than allowlist-based: `self/` lives outside `.claude/` (the marketplace + `source`), so it is physically absent from any consumer's plugin cache — no risk of + accidental inclusion via a future auto-discovery change. ## Enabling in a consuming project diff --git a/.claude/gates.json b/.claude/gates.json index 910242e..4a67858 100644 --- a/.claude/gates.json +++ b/.claude/gates.json @@ -79,6 +79,6 @@ "_plan_note": "issue #100 — optional spec/plan gate before implementation starts. plan.gate: off (default; today's single-pass advance behavior, unchanged) | label (gate only planned issues that ALSO carry a plan-first label) | always (gate every planned+module:* issue). Unknown/missing value falls back to off. When gated, the loop posts ONE structured plan comment on the issue (marked ``) and labels it plan-review + needs-human instead of implementing; the owner reviews the comment and either replaces plan-review with plan-approved (approve — the next tick implements with the plan injected into the implementer and reviewers as authoritative scope) or removes plan-review (request changes — the loop re-plans). Owner-only approval is NOT identity-enforced — same limitation as the `planned` label itself (see docs/USAGE.md). Read by loop-census.sh/loop-tick.sh/loop-event.sh.", "plan": { "gate": "off" }, - "_protectedPaths_note": "issue #94 Layer 2 — deterministic protected-paths guard. Array of glob patterns (\"*\" = one path segment, \"**\" = any depth). An agent-authored PR whose diff touches ANY matching path is NEVER auto-merged: merge-ready.sh blocks the merge and labels the PR needs-human, and reviewers hard-reject it (see .claude/agents/reviewer.md). Protects the loop's own control plane from an injected instruction that tries to edit it. EMPTY ARRAY = disabled (same empty-means-skip convention as `gates`/`notify`) — self-hosted repos whose harness files ARE the product override this to [] in .claude/self/gates.json. Read from the adapter resolved via GATES_FILE (falls back to this root adapter). Documented in docs/HARDENING.md.", + "_protectedPaths_note": "issue #94 Layer 2 — deterministic protected-paths guard. Array of glob patterns (\"*\" = one path segment, \"**\" = any depth). An agent-authored PR whose diff touches ANY matching path is NEVER auto-merged: merge-ready.sh blocks the merge and labels the PR needs-human, and reviewers hard-reject it (see .claude/agents/reviewer.md). Protects the loop's own control plane from an injected instruction that tries to edit it. EMPTY ARRAY = disabled (same empty-means-skip convention as `gates`/`notify`) — self-hosted repos whose harness files ARE the product override this to [] in self/gates.json. Read from the adapter resolved via GATES_FILE (falls back to this root adapter). Documented in docs/HARDENING.md.", "protectedPaths": [".claude/**", ".github/workflows/**", "gates.json", "**/gates.json"] } diff --git a/.claude/scripts/arm-loop.sh b/.claude/scripts/arm-loop.sh index 2206221..7be3fbf 100755 --- a/.claude/scripts/arm-loop.sh +++ b/.claude/scripts/arm-loop.sh @@ -16,7 +16,7 @@ # bash .claude/scripts/arm-loop.sh [--gates-file ] [--permission-mode ] [--capacity N] [--rc-name ] [--spawn ] [--stop-after-days N] # # --gates-file passed to pr-loop.service as GATES_FILE (e.g. -# .claude/self/gates.json for the self-hosted +# self/gates.json for the self-hosted # loop). Omit for the default project adapter. # --permission-mode passed to `claude remote-control --permission-mode`. # Defaults to permissions.defaultMode in diff --git a/.claude/scripts/cockpit.sh b/.claude/scripts/cockpit.sh index e242845..43cf72c 100755 --- a/.claude/scripts/cockpit.sh +++ b/.claude/scripts/cockpit.sh @@ -166,7 +166,7 @@ gh() { "$gh_bin" "$@"; } # Adapter to read for model/skill routing — same override contract as # gate.sh/worktree.sh (GATES_FILE env, relative paths resolve from repo root), -# so self-hosting this repo can point it at .claude/self/gates.json. +# so self-hosting this repo can point it at self/gates.json. gates_ref="${GATES_FILE:-.claude/gates.json}" case "$gates_ref" in /*) gates="$gates_ref" ;; diff --git a/.claude/scripts/cockpit.test.sh b/.claude/scripts/cockpit.test.sh index d1a0cf0..65afd1b 100755 --- a/.claude/scripts/cockpit.test.sh +++ b/.claude/scripts/cockpit.test.sh @@ -25,8 +25,8 @@ set -uo pipefail # Isolate from the CALLER's environment: this test is now wired into -# .claude/self/checks.sh's `test` case, which itself typically runs under -# `GATES_FILE=.claude/self/gates.json` (the self-host loop). Since env vars +# self/checks.sh's `test` case, which itself typically runs under +# `GATES_FILE=self/gates.json` (the self-host loop). Since env vars # set before a command propagate to every child process it spawns, an # ambient GATES_FILE would silently redirect the DEFAULT-adapter assertions # below (section 2) onto the self-adapter. Section 3 sets GATES_FILE @@ -466,8 +466,8 @@ check "all-clear run: no awaiting-your-review group heading" bash -c '! grep -qF # (no gh/network either way). # --------------------------------------------------------------------------- html_self="$work/cockpit-self.html" -GATES_FILE=.claude/self/gates.json bash "$cockpit" --fixtures "$work/fixtures" "$html_self" >/dev/null 2>"$work/stderr-self.log" -check "GATES_FILE override honored in generator output" grep -q 'Adapter: .claude/self/gates.json' "$html_self" +GATES_FILE=self/gates.json bash "$cockpit" --fixtures "$work/fixtures" "$html_self" >/dev/null 2>"$work/stderr-self.log" +check "GATES_FILE override honored in generator output" grep -q 'Adapter: self/gates.json' "$html_self" # --------------------------------------------------------------------------- # 4. Graceful degrade when gh is unavailable — stub COCKPIT_GH_BIN so this is @@ -658,7 +658,7 @@ alias_npm_pid="" # event timeline + latest breadcrumbs + live worktree forensics, entirely # offline. Forensics are computed by cockpit-serve.sh shelling out to git # against a SYNTHETIC temp git repo/worktree built here under $TMPDIR -# (mirrors .claude/self/smoke-fanout.sh's own git-init/worktree-add +# (mirrors self/smoke-fanout.sh's own git-init/worktree-add # pattern) -- never against this checkout, so this stays deterministic and # isolated. Covers BOTH worktree-lookup strategies documented in # cockpit-serve.sh: by conventional directory name diff --git a/.claude/scripts/gate.sh b/.claude/scripts/gate.sh index 3fbd0ff..962a56d 100755 --- a/.claude/scripts/gate.sh +++ b/.claude/scripts/gate.sh @@ -13,8 +13,8 @@ key="${1:?usage: gate.sh }" . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-roots.sh" # Which adapter to read. Defaults to the project adapter; set GATES_FILE to run a -# different one (e.g. GATES_FILE=.claude/self/gates.json for the self-host loop — -# see .claude/self/README.md). Relative paths resolve from the repo root. +# different one (e.g. GATES_FILE=self/gates.json for the self-host loop — +# see self/README.md). Relative paths resolve from the repo root. gates_ref="${GATES_FILE:-.claude/gates.json}" case "$gates_ref" in /*) gates="$gates_ref" ;; diff --git a/.claude/scripts/hooks-parity.test.sh b/.claude/scripts/hooks-parity.test.sh index 595d49b..37057b2 100755 --- a/.claude/scripts/hooks-parity.test.sh +++ b/.claude/scripts/hooks-parity.test.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # hooks-parity.test.sh — offline smoke test for the hooks-parity check -# (issue #140) added to .claude/self/checks.sh's `do_hooks_parity` (run as +# (issue #140) added to self/checks.sh's `do_hooks_parity` (run as # part of `do_build`, and therefore under `gate.sh build`/`gate.sh test`). # # Asserts: @@ -21,7 +21,7 @@ set -uo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "$script_dir/../.." && pwd)" -checks_sh="$repo_root/.claude/self/checks.sh" +checks_sh="$repo_root/self/checks.sh" work="$(mktemp -d "${TMPDIR:-/tmp}/hooks-parity-test.XXXXXX")" trap 'rm -rf "$work"' EXIT diff --git a/.claude/scripts/loop-census.sh b/.claude/scripts/loop-census.sh index 54b5ab2..30eb308 100644 --- a/.claude/scripts/loop-census.sh +++ b/.claude/scripts/loop-census.sh @@ -109,7 +109,7 @@ # # The module label set is derived from $GATES_FILE (default .claude/gates.json) # → modules[].name, so the same script serves the self-hosted loop -# (GATES_FILE=.claude/self/gates.json) and downstream adopters. +# (GATES_FILE=self/gates.json) and downstream adopters. # # WHY THIS EXISTS (issue: loop stalled 13h with two planned issues): ticks that # "optimized" STEP 0 away — or piped the cursor-advancing notify-poll.sh through diff --git a/.claude/scripts/loop-census.test.sh b/.claude/scripts/loop-census.test.sh index 1df4e9c..ed42f46 100644 --- a/.claude/scripts/loop-census.test.sh +++ b/.claude/scripts/loop-census.test.sh @@ -225,7 +225,7 @@ git -C "$fixture" branch feat/issue-100-w main >/dev/null # Unset GATES_FILE explicitly: loop-census.sh reads it straight from the # environment, and this test may itself be run from inside a gate invocation -# that exports GATES_FILE=.claude/self/gates.json for the OUTER repo — which +# that exports GATES_FILE=self/gates.json for the OUTER repo — which # would leak in here and make census look for a gates.json this fixture never # created. Force it back to the fixture's own default-relative gates.json. out="$(env -u GATES_FILE bash "$scripts_dir/loop-census.sh" "acme/repo")" diff --git a/.claude/scripts/loop-daemon.sh b/.claude/scripts/loop-daemon.sh index 15462d0..1325942 100644 --- a/.claude/scripts/loop-daemon.sh +++ b/.claude/scripts/loop-daemon.sh @@ -106,7 +106,7 @@ # Env: # LOOP_MODEL model for the driver (default sonnet; read by loop-event.sh) # GATES_FILE adapter override, passed straight through the environment -# (self-hosting: .claude/self/gates.json) +# (self-hosting: self/gates.json) # LOOP_DRIVER_TIMEOUT wall-clock cap per driver (default 90m); becomes # systemd-run's `-p RuntimeMaxSec=` when systemd-run # is on PATH, else `timeout`'s duration (issue #119) diff --git a/.claude/scripts/loop-event.sh b/.claude/scripts/loop-event.sh index 5a15300..ad454c6 100644 --- a/.claude/scripts/loop-event.sh +++ b/.claude/scripts/loop-event.sh @@ -131,7 +131,7 @@ mode="$(printf '%s\n' "$tick_out" | sed -n 's/^advance_mode=//p' | tail -1)" mode="${mode:-implement}" # Adapter clause: only when this loop runs against a non-default adapter -# (self-hosting). Mirrors the wording in .claude/self/pr-loop-self.md. +# (self-hosting). Mirrors the wording in self/pr-loop-self.md. adapter="" if [ -n "${GATES_FILE:-}" ]; then adapter="Export GATES_FILE=$GATES_FILE for every gate/orchestration step, and instruct every spawned agent (orchestrator, implementers, reviewers) to read $GATES_FILE — NOT the placeholder root .claude/gates.json — as its adapter (module map, gates, review lenses). Every gate.sh invocation MUST run as: GATES_FILE=$GATES_FILE bash $script_dir/gate.sh . " @@ -201,7 +201,7 @@ NEVER force-merge, and NEVER merge, in either outcome." Issue #$n was flagged STALLED by loop-census.sh (no recorded activity for the adapter's stall_minutes window) and a PRIOR driver already left partial work on a local \`feat/issue-$n-*\` branch. $branch_clause This is a CONTINUATION, NOT a fresh advance: do NOT create a new branch, do NOT re-scope the issue from scratch, and do NOT start a new worktree implementer with a clean slate — inspect what is ALREADY THERE (git log/diff on that branch) and pick up from it. 1. Locate and reattach to the EXISTING worktree for that branch (or re-create a worktree checked out on the SAME branch if the prior one was already cleaned up) — the commits/WIP on the branch are the authoritative starting point. 2. Assess what's actually done vs. still missing against the issue's acceptance criteria, then finish the implementation on that SAME branch. -3. Re-run the module's gates before publishing: \`GATES_FILE=.claude/self/gates.json bash $script_dir/gate.sh \` for build/lint/typecheck/test/coverage per the adapter (per the adapter clause above) — do not skip this just because some gates may have passed in a prior, now-stale attempt. +3. Re-run the module's gates before publishing: \`GATES_FILE=self/gates.json bash $script_dir/gate.sh \` for build/lint/typecheck/test/coverage per the adapter (per the adapter clause above) — do not skip this just because some gates may have passed in a prior, now-stale attempt. 4. Route the finished diff through the reviewer lenses per the adapter (consensus as configured), addressing any reject before proceeding. 5. Publish: push the branch and open the bot PR if none exists yet (\`bash $script_dir/bot-gh.sh pr create ...\`), or push the update to refresh an already-open PR for this issue. The work product MUST land on GitHub before you end your turn — a resume that ends without a pushed branch/PR just re-stalls the issue for the next tick. Do NOT merge." diff --git a/.claude/scripts/loop-event.test.sh b/.claude/scripts/loop-event.test.sh index cef0c99..0812e1e 100644 --- a/.claude/scripts/loop-event.test.sh +++ b/.claude/scripts/loop-event.test.sh @@ -288,10 +288,10 @@ check "scenario 5: no prompt-file line emitted" bash -c '! printf "%s\n" "$1" | # --------------------------------------------------------------------------- dir6="$(new_fixture scenario6 'cadence=FAST cron=* * * * * action=advance issue=9')" -out6="$(cd "$dir6" && PATH="/usr/bin:/bin" GATES_FILE=.claude/self/gates.json bash .claude/scripts/loop-event.sh)"; rc6=$? +out6="$(cd "$dir6" && PATH="/usr/bin:/bin" GATES_FILE=self/gates.json bash .claude/scripts/loop-event.sh)"; rc6=$? pf6="$(printf '%s\n' "$out6" | sed -n 's/^loop-event: prompt-file=//p')" check "scenario 6 (GATES_FILE): exits 0" [ "$rc6" -eq 0 ] -check "scenario 6: prompt file exports the adapter's GATES_FILE" bash -c 'grep -q "GATES_FILE=.claude/self/gates.json" "$1"' _ "$pf6" +check "scenario 6: prompt file exports the adapter's GATES_FILE" bash -c 'grep -q "GATES_FILE=self/gates.json" "$1"' _ "$pf6" echo "" if [ "$fail" -eq 0 ]; then diff --git a/.claude/scripts/merge-ready.sh b/.claude/scripts/merge-ready.sh index b297a4b..59a3adc 100644 --- a/.claude/scripts/merge-ready.sh +++ b/.claude/scripts/merge-ready.sh @@ -43,7 +43,7 @@ repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" if [ -f "$script_dir/needs-human.sh" ]; then . "$script_dir/needs-human.sh"; fi owner="${MERGE_APPROVER:-${repo%%/*}}" # the approver whose APPROVED review authorizes a merge -# Adapter file: honor GATES_FILE (the self-host loop points at .claude/self/gates.json), +# Adapter file: honor GATES_FILE (the self-host loop points at self/gates.json), # fall back to the shipped root adapter. Both merge.baseBranch and protectedPaths # (issue #94 Layer 2) are read from it, so the self-adapter's permissive protectedPaths # override applies when the loop runs self-hosted. diff --git a/.claude/scripts/needs-human.test.sh b/.claude/scripts/needs-human.test.sh index 9532eff..734377d 100755 --- a/.claude/scripts/needs-human.test.sh +++ b/.claude/scripts/needs-human.test.sh @@ -26,8 +26,8 @@ set -uo pipefail # Isolate from the CALLER's environment (mirrors cockpit.test.sh/notify.test.sh): -# this test is wired into .claude/self/checks.sh's `test` case, which itself -# often runs under `GATES_FILE=.claude/self/gates.json` (the self-host loop). +# this test is wired into self/checks.sh's `test` case, which itself +# often runs under `GATES_FILE=self/gates.json` (the self-host loop). # An ambient GATES_FILE would silently redirect notify.sh's config lookup # (called by needs_human_flag/needs_human_clear below) onto the SELF adapter # instead of each fixture's own hand-written .claude/gates.json. diff --git a/.claude/scripts/notify.test.sh b/.claude/scripts/notify.test.sh index 0bf3d23..036cf21 100755 --- a/.claude/scripts/notify.test.sh +++ b/.claude/scripts/notify.test.sh @@ -12,8 +12,8 @@ set -uo pipefail # Isolate from the CALLER's environment (mirrors cockpit.test.sh): this test -# is wired into .claude/self/checks.sh's `test` case, which itself often runs -# under `GATES_FILE=.claude/self/gates.json` (the self-host loop). Since env +# is wired into self/checks.sh's `test` case, which itself often runs +# under `GATES_FILE=self/gates.json` (the self-host loop). Since env # vars set before a command propagate to every child process, an ambient # GATES_FILE would silently redirect notify.sh's config lookup onto the SELF # adapter instead of each fixture's own hand-written .claude/gates.json below. diff --git a/.claude/scripts/plan-gate.test.sh b/.claude/scripts/plan-gate.test.sh index 77b3891..57e5097 100755 --- a/.claude/scripts/plan-gate.test.sh +++ b/.claude/scripts/plan-gate.test.sh @@ -108,7 +108,7 @@ EOF run_census() { # $1 = fixture dir. GATES_FILE unset explicitly (a test invoked from inside - # a GATES_FILE=.claude/self/gates.json gate run must not leak that into the + # a GATES_FILE=self/gates.json gate run must not leak that into the # fixture's own default-relative gates.json — same guard as # loop-census.test.sh). env -u GATES_FILE bash "$1/.claude/scripts/loop-census.sh" "acme/repo" diff --git a/.claude/scripts/pr-comment-fix.test.sh b/.claude/scripts/pr-comment-fix.test.sh index 8fbce5e..0ddc21e 100644 --- a/.claude/scripts/pr-comment-fix.test.sh +++ b/.claude/scripts/pr-comment-fix.test.sh @@ -189,7 +189,7 @@ EOF chmod +x "$scripts_dir/pr-feedback.sh" # Unset GATES_FILE explicitly: this test may itself run from inside a gate -# invocation that exports GATES_FILE=.claude/self/gates.json for the OUTER +# invocation that exports GATES_FILE=self/gates.json for the OUTER # repo (mirrors loop-census.test.sh's own guard) — which would leak in here # and make pr-comment-fix.sh read the outer self-adapter's commentFix.botAllowlist # instead of this fixture's own gates.json. diff --git a/.claude/scripts/sync-managed-files.test.sh b/.claude/scripts/sync-managed-files.test.sh index de95eae..73ff92a 100755 --- a/.claude/scripts/sync-managed-files.test.sh +++ b/.claude/scripts/sync-managed-files.test.sh @@ -447,7 +447,7 @@ JSON # takes an absolute GATES_FILE as-is, same convention as gate.sh, see gate.test.sh's # write_gates_file/run_gate). Scenarios 12/12b/15 must be deterministic regardless of an # ambient GATES_FILE leaking in from the caller's environment (e.g. the self-hosted test -# gate runs *.test.sh with GATES_FILE=.claude/self/gates.json already exported) — without +# gate runs *.test.sh with GATES_FILE=self/gates.json already exported) — without # this pin, derive_module_labels would resolve the ambient adapter relative to $t12 instead # of this fixture's own gates.json and silently fail to derive any labels. out12="$(GATES_FILE="$t12/.claude/gates.json" bash "$plugin_v_ok/skills/sync/sync.sh" "$t12" 2>&1)" @@ -597,7 +597,7 @@ cat >"$t9/.claude/gates.json" <<'JSON' JSON # Pin GATES_FILE to this self-hosting fixture's OWN adapter (absolute path — same isolation # rationale as scenario 12) so this scenario is deterministic regardless of an ambient -# GATES_FILE (e.g. the real self-hosted test gate exports GATES_FILE=.claude/self/gates.json +# GATES_FILE (e.g. the real self-hosted test gate exports GATES_FILE=self/gates.json # before invoking this file, which would otherwise make derive_module_labels look in the # wrong place relative to $t9 and change which advisory lines print). out15="$(GATES_FILE="$t9/.claude/gates.json" bash "$t9/.claude/skills/sync/sync.sh" "$t9" 2>&1)" diff --git a/.claude/scripts/worktree.sh b/.claude/scripts/worktree.sh index 2f9272e..09c59c4 100755 --- a/.claude/scripts/worktree.sh +++ b/.claude/scripts/worktree.sh @@ -39,7 +39,7 @@ if [ "$phase" = setup ]; then fi fi # Which adapter to read. Defaults to the project adapter; set GATES_FILE to run a -# different one (e.g. GATES_FILE=.claude/self/gates.json). Relative paths resolve +# different one (e.g. GATES_FILE=self/gates.json). Relative paths resolve # from the repo root. gates_ref="${GATES_FILE:-.claude/gates.json}" case "$gates_ref" in diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index c6b6eb6..3249c25 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -191,7 +191,7 @@ Two ways to fire the loop; ask the user which one (`AskUserQuestion`), presentin ``` bash .claude/scripts/arm-loop.sh ``` - (Self-hosting: `bash .claude/scripts/arm-loop.sh --gates-file .claude/self/gates.json`.) This one script + (Self-hosting: `bash .claude/scripts/arm-loop.sh --gates-file self/gates.json`.) This one script installs both `pr-loop-.service` (the loop daemon) and `claude-rc-.service` (`claude remote-control` in a detached tmux session, so planning sessions can be spawned from claude.ai/mobile), enables + starts them, and runs `loginctl enable-linger $USER` so they keep running without an open login diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index a0ef7ef..dc42b2b 100644 --- a/.github/workflows/gates.yml +++ b/.github/workflows/gates.yml @@ -45,8 +45,8 @@ jobs: # 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,5 +64,5 @@ jobs: # 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 }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7fdd11..b022a8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing to reCode reCode dogfoods itself: this repo's own backlog runs through the same orchestrator/implementer/reviewer -pipeline it ships to consumers, using a self-hosted adapter (`.claude/self/gates.json`) instead of the +pipeline it ships to consumers, using a self-hosted adapter (`self/gates.json`) instead of the placeholder `.claude/gates.json` that downstream projects fill in. Understanding that loop is most of what you need to contribute here. See [`docs/USAGE.md`](docs/USAGE.md) for the full mechanics; this file is the short, contributor-facing version plus the boundaries specific to this repo. @@ -16,7 +16,7 @@ queue, **never** picks up a `backlog` issue. this label** — no contributor, agent, or bot ever assigns it, to their own issue or anyone else's. Commenting "approved" on an issue does nothing; nothing watches issue text, only the label. - **`module:`** — routing, not approval. It maps an issue to one of this repo's self-hosted modules - (`docs`, `harness` → `.claude`, `examples`, `ci` → `.github` — see `.claude/self/gates.json`) and therefore + (`docs`, `harness` → `.claude`, `examples`, `ci` → `.github` — see `self/gates.json`) and therefore to a worker's non-overlapping boundary. No module label ⇒ nothing safe to hand a worker, `planned` or not. **If you want to propose work:** open an issue describing it, labelled `backlog` (+ a `module:*` label if you @@ -29,9 +29,9 @@ loop-eligible. 2. **Build** — each sub-task goes to an `implementer`, isolated in its own git worktree/branch. Implementers must stay inside their assigned module's `path` — cross-module work gets re-scoped, never reached across. 3. **Gate** — before anything is called done it must pass this repo's self-hosted gates - (`GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh `), enforced by a `Stop` + (`GATES_FILE=self/gates.json bash .claude/scripts/gate.sh `), enforced by a `Stop` hook, not by asking the model nicely. -4. **Review** — gated changes go to reviewer lenses (`correctness`, `tests` per `.claude/self/gates.json` → +4. **Review** — gated changes go to reviewer lenses (`correctness`, `tests` per `self/gates.json` → `review.lenses`), adversarial and one-per-lens. Consensus mode is `all`: every lens must approve. 5. **PR** — opened by a **bot machine account** (`.claude/scripts/bot-gh.sh`), never under the owner's own `gh` auth — GitHub blocks a PR author from approving their own PR, and the owner's approval is the only @@ -45,10 +45,10 @@ run the self-hosted gates yourself before requesting review (see [Running the ga below). ## What NOT to touch -- **`.claude/self/`** — this repo's own self-hosting adapter and loop config (module map, gate commands, +- **`self/`** — this repo's own self-hosting adapter and loop config (module map, gate commands, review/merge settings for reCode's *own* backlog). It is deliberately kept separate from the shipped `.claude/gates.json` placeholder so that a downstream project cloning/installing this plugin never inherits - reCode's own configuration (see `.claude/self/README.md`). Treat it as owner-maintained; if a change to the + reCode's own configuration (see `self/README.md`). Treat it as owner-maintained; if a change to the self-adapter is genuinely part of your task, call it out explicitly rather than editing it incidentally. - **MANAGED files** — templates under `.claude/skills/setup/templates/` (`feature-fanout.js`, `pr-loop.service`, `claude-rc.service`, `arm-loop.sh`) each carry an `@orchestrator-managed vN` @@ -65,9 +65,9 @@ below). ## Running the gates This repo's own gates run through the self-adapter, not the placeholder `.claude/gates.json`: ```bash -GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build -GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh lint -GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh test +GATES_FILE=self/gates.json bash .claude/scripts/gate.sh build +GATES_FILE=self/gates.json bash .claude/scripts/gate.sh lint +GATES_FILE=self/gates.json bash .claude/scripts/gate.sh test ``` `build`/`lint` are static checks (every adapter JSON is well-shaped; every shell script parses; every workflow `.js` type-checks with `node --check`). `test` additionally runs a deterministic end-to-end smoke of diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 1ec6de7..974db03 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -205,7 +205,7 @@ model (the `module:*` opt-in queue + the owner-approval merge gate) that this ch supervised by `systemd --user`, survives Claude Code restarts, adaptive FAST/WATCH/IDLE sleep read straight off the census) and `claude-rc-.service` (`claude remote-control` in a detached tmux session, for spawning planning sessions remotely). Self-hosting: add `--gates-file - .claude/self/gates.json`. See [`USAGE.md` → "Cron-less loop + self/gates.json`. See [`USAGE.md` → "Cron-less loop (daemon)"](USAGE.md#cron-less-loop-daemon) for the full architecture, cadence, run ledger, and failure-contract details, including WSL2's `systemd=true` prerequisite. Fallback (no systemd available): **`/orchestrator:pr-loop`** — the legacy session-scoped cron. It diff --git a/docs/HARDENING.md b/docs/HARDENING.md index a59714c..cb574c5 100644 --- a/docs/HARDENING.md +++ b/docs/HARDENING.md @@ -57,7 +57,7 @@ regardless of owner approval or green CI, and reviewers hard-reject it outright `.claude/agents/reviewer.md`). The shipped root adapter defaults this to a protective set (`.claude/**`, `.github/workflows/**`, `gates.json`, `**/gates.json`) so downstream adopters' harness and CI files can't be silently rewritten by an agent. This repo's own self-adapter -(`.claude/self/gates.json`) overrides it to an empty array, which disables the guard — reCode's harness +(`self/gates.json`) overrides it to an empty array, which disables the guard — reCode's harness files under `.claude/` ARE the product, so legitimate slices of work must remain mergeable when the loop runs self-hosted. A fuller writeup is deferred to follow-up issue #166. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 59ba0ef..e06817f 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -49,8 +49,8 @@ frozen copy means you never get fixes/improvements: - `.claude/agents/` — orchestrator, implementer, reviewer, test-runner. - `.claude/commands/` — `pr-loop.md`, `harden.md`, `test-pr.md`, etc. (they resolve as namespaced `/orchestrator:*` commands once the plugin is enabled). Note: `pr-loop-self.md` is **not** - among these — it lives under `.claude/self/` (not `.claude/commands/`) and is not plugin-distributed; see - `.claude/self/README.md` if your repo has a self-hosting setup of its own. + among these — it lives under `self/` (not `.claude/commands/`) and is not plugin-distributed; see + `self/README.md` if your repo has a self-hosting setup of its own. - `.claude/skills/` — e.g. the `setup` skill. - `.claude/hooks/` — `hooks.json` (the lint/test-affected wiring is now shipped by the plugin and resolves via `${CLAUDE_PLUGIN_ROOT}` automatically). diff --git a/docs/USAGE.md b/docs/USAGE.md index 4de2108..bc48838 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -256,7 +256,7 @@ detached tmux session all touch `$HOME`/systemd, which the Claude Code sandbox b bash .claude/scripts/arm-loop.sh [--gates-file ] [--permission-mode ] [--capacity N] ``` **in a real terminal outside Claude Code.** It's idempotent (safe to re-run any time). Self-hosting: add -`--gates-file .claude/self/gates.json`. WSL2-only extra: optionally make the loop survive a Windows reboot +`--gates-file self/gates.json`. WSL2-only extra: optionally make the loop survive a Windows reboot **unattended** — i.e. WSL2 boots at system startup, before anyone logs in, not just at logon. From an **elevated Windows PowerShell** (Run as Administrator — required by `-RunLevel Highest`), substituting `` from `wsl -l` and `` for your Linux username: @@ -428,12 +428,12 @@ it, since the driving session still has to read the verdict and act on it, e.g. > Historical note: before the `planned` label existed, the `module:*` label alone was the opt-in queue. > If a repo predates the split, treat `module:*`-only issues as `backlog` until the owner adds `planned`. -**Self-hosting this repo's own backlog?** **`.claude/self/pr-loop-self.md`** runs the same loop mechanics +**Self-hosting this repo's own backlog?** **`self/pr-loop-self.md`** runs the same loop mechanics self-hosted, against this repo's own `.claude`/`docs`/`examples`/`.github` backlog, using -**`.claude/self/gates.json`** as the adapter (module map, gates, review lenses) instead of the placeholder -`.claude/gates.json` above. It lives under `.claude/self/` (not `.claude/commands/`), so it is self-hosting-only +**`self/gates.json`** as the adapter (module map, gates, review lenses) instead of the placeholder +`.claude/gates.json` above. It lives under `self/` (not `.claude/commands/`), so it is self-hosting-only — not a registered slash command and never packaged to downstream installs of the plugin; ask Claude to read and -follow it directly. See `.claude/self/README.md` for the self-adapter contract. +follow it directly. See `self/README.md` for the self-adapter contract. New project? Wire this up with the **[new-project configuration checklist](GETTING_STARTED.md#new-project-configuration-checklist)**. diff --git a/examples/README.md b/examples/README.md index eda6eeb..5cbadce 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,5 +13,5 @@ Each subdirectory is one stack; the `gates.json` there is what you'd drop into y > (`bash .claude/scripts/gate.sh build`, etc.). Also here, but **not** a worked example: [`fixture-target/`](fixture-target/) — the tiny *runnable* fixture -repo the self-host smoke harness (`.claude/self/smoke-fanout.sh`, issue #64) drives end-to-end on every PR. +repo the self-host smoke harness (`self/smoke-fanout.sh`, issue #64) drives end-to-end on every PR. See its README; don't copy it as an adapter starting point. diff --git a/examples/fixture-target/README.md b/examples/fixture-target/README.md index c8abfbd..fae8303 100644 --- a/examples/fixture-target/README.md +++ b/examples/fixture-target/README.md @@ -1,7 +1,7 @@ # fixture-target — smoke-test fixture for the fan-out scaffold A deliberately tiny, consumer-shaped target repo used by the self-host smoke harness -(`.claude/self/smoke-fanout.sh`, issue #64 — Phase 2 of #11). It is **not** a worked example to copy +(`self/smoke-fanout.sh`, issue #64 — Phase 2 of #11). It is **not** a worked example to copy (see `ts-solidity-foundry/` for that); it exists so every PR to this repo can validate the deterministic fan-out scaffolding end-to-end **on a fixture, not on the harness itself** (no bootstrap regress) and **without live agents** (no tokens, no nondeterminism — CI-safe). @@ -11,7 +11,7 @@ What the smoke harness does with it: 1. Stages `src/`, `test/`, and `gates.json` (as `.claude/gates.json`) plus the **real** `gate.sh` into a temp git repo — the same layout a consumer repo has. 2. Plays a *recorded implementer*: isolated worktree → applies a canned diff - (`.claude/self/smoke/implementer.patch`) → asserts the diff stays inside the `fixture-core` + (`self/smoke/implementer.patch`) → asserts the diff stays inside the `fixture-core` module boundary (`src/`). 3. Runs the fixture's `build`/`lint`/`test` gates through `gate.sh` in the worktree (plus an empty gate to prove the skip path), merges the branch, and asserts the change landed on `main`. diff --git a/examples/fixture-target/gates.json b/examples/fixture-target/gates.json index fca8a70..7dcd9c7 100644 --- a/examples/fixture-target/gates.json +++ b/examples/fixture-target/gates.json @@ -1,5 +1,5 @@ { - "_README": "Adapter for the SMOKE-TEST FIXTURE TARGET (issue #64, Phase 2 of #11). A minimal consumer-shaped repo the smoke harness (.claude/self/smoke-fanout.sh) stages into a temp git repo and drives end-to-end: worktree -> canned diff -> these gates via the real gate.sh -> merge. Gates are node-only so they run in a bare environment.", + "_README": "Adapter for the SMOKE-TEST FIXTURE TARGET (issue #64, Phase 2 of #11). A minimal consumer-shaped repo the smoke harness (self/smoke-fanout.sh) stages into a temp git repo and drives end-to-end: worktree -> canned diff -> these gates via the real gate.sh -> merge. Gates are node-only so they run in a bare environment.", "project": { "name": "fixture-target", "language": "js", "packageManager": "none" }, diff --git a/.claude/self/README.md b/self/README.md similarity index 67% rename from .claude/self/README.md rename to self/README.md index 213a04e..45a325a 100644 --- a/.claude/self/README.md +++ b/self/README.md @@ -3,6 +3,12 @@ This directory lets **this repo run its own PR loop against its own harness/docs**, without touching the shipped placeholder `.claude/gates.json` (which stays pristine for downstream adopters). +**Lives at the top-level `self/` (issue #138), not under `.claude/`.** The plugin marketplace sources the +payload from `./.claude` (see `.claude-plugin/marketplace.json`), so anything outside `.claude/` is +structurally never copied into a consumer's plugin cache — no allowlist or naming convention to maintain, +just physical location. This directory used to live at `.claude/self/`; it moved out entirely so a freshly +built plugin cache contains none of this repo's self-development assets. + ## Why a separate adapter `.claude/gates.json` is the file a *new project* fills in. If we filled it with this repo's own modules and gates, every clone of the template would inherit our self-config. So the self-config lives here instead, and @@ -27,37 +33,36 @@ the tooling reads it only when explicitly pointed at it. `gate.sh` honors a `GATES_FILE` env override (defaults to `.claude/gates.json`): ```bash -GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build -GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh lint -GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh test +GATES_FILE=self/gates.json bash .claude/scripts/gate.sh build +GATES_FILE=self/gates.json bash .claude/scripts/gate.sh lint +GATES_FILE=self/gates.json bash .claude/scripts/gate.sh test ``` ## Running the loop self-hosted To have the autonomous loop work this repo's own `module:*` backlog: 1. Label the target issue with a self module — `module:docs`, `module:harness`, `module:examples`, or `module:ci`. -2. Drive the tick with `GATES_FILE=.claude/self/gates.json` exported, and tell the orchestrator to read - **`.claude/self/gates.json`** as its adapter (module map + gates) for this repo. The generic agents/scripts +2. Drive the tick with `GATES_FILE=self/gates.json` exported, and tell the orchestrator to read + **`self/gates.json`** as its adapter (module map + gates) for this repo. The generic agents/scripts otherwise behave identically — worker boundaries come from this file's `modules`, gates from its `gates`. -**Recommended (issue #102): the cron-less daemon.** `bash .claude/scripts/arm-loop.sh --gates-file .claude/self/gates.json` +**Recommended (issue #102): the cron-less daemon.** `bash .claude/scripts/arm-loop.sh --gates-file self/gates.json` (run in a real terminal outside Claude Code — installing systemd units/`enable-linger`/tmux touches `$HOME` and is blocked by the sandbox) arms `.claude/scripts/loop-daemon.sh` under `systemd --user`, forever, adapted to this repo's own module set. It survives session restarts and spawns a driver only on an actionable -verdict. Never run it alongside `.claude/self/pr-loop-self.md`'s cron at the same time — safe (spawn lock), +verdict. Never run it alongside `self/pr-loop-self.md`'s cron at the same time — safe (spawn lock), just wasteful. -The legacy, session-scoped way to do this is **`.claude/self/pr-loop-self.md`** — it mirrors `/pr-loop` exactly +The legacy, session-scoped way to do this is **`self/pr-loop-self.md`** — it mirrors `/pr-loop` exactly (arm/re-arm cron, adaptive cadence, poll → merge → address-feedback → advance) but carries -`GATES_FILE=.claude/self/gates.json` through every gate call and every spawned agent, and adapts on the self +`GATES_FILE=self/gates.json` through every gate call and every spawned agent, and adapts on the self modules (`module:docs`/`module:harness`/`module:examples`/`module:ci`) instead of the project's own `gates.json`. It uses a distinct cron identity marker ("self-hosted autonomous PR loop") so it never collides with a `/pr-loop` job in the same session. -This file lives under `.claude/self/` (not `.claude/commands/`) so it is **not** shipped to downstream plugin -consumers — see `.claude/.claude-plugin/plugin.json`'s explicit `commands` allowlist (issue #76). It is -therefore not a registered `/pr-loop-self` slash command; to (re)arm the self-hosted loop, ask Claude to read -and follow the instructions in `.claude/self/pr-loop-self.md` directly (e.g. "read and run -`.claude/self/pr-loop-self.md`"). +This file lives under the top-level `self/` (outside `.claude/`, so structurally **not** shipped to +downstream plugin consumers — issue #138) rather than `.claude/commands/`, so it is also not a registered +`/pr-loop-self` slash command; to (re)arm the self-hosted loop, ask Claude to read and follow the +instructions in `self/pr-loop-self.md` directly (e.g. "read and run `self/pr-loop-self.md`"). ## Self-hosting promotion (the one gotcha) Agent-definition / `settings.json` / hook changes only take effect on a **fresh session**. So when the loop diff --git a/.claude/self/checks.sh b/self/checks.sh similarity index 93% rename from .claude/self/checks.sh rename to self/checks.sh index 9ba1ddf..c23f935 100644 --- a/.claude/self/checks.sh +++ b/self/checks.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Self-host gate implementations (issue #11). node + bash only — no external # linters — so the loop can validate harness changes in a bare environment. -# Invoked via .claude/self/gates.json, e.g. `bash .claude/self/checks.sh lint`. +# Invoked via self/gates.json, e.g. `bash self/checks.sh lint`. # # build → every JSON config parses and each adapter has the required shape # lint → `bash -n` every shell script + `node --check` every workflow @@ -12,7 +12,7 @@ # this file again. set -uo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$root" cmd="${1:?usage: checks.sh build|lint|test}" @@ -20,13 +20,13 @@ json_parse() { node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'u do_build() { local rc=0 - for f in .claude/gates.json .claude/self/gates.json .claude/settings.json .claude/.claude-plugin/plugin.json .claude/.claude-plugin/marketplace.json .claude/hooks/hooks.json; do + for f in .claude/gates.json self/gates.json .claude/settings.json .claude/.claude-plugin/plugin.json .claude/.claude-plugin/marketplace.json .claude/hooks/hooks.json; do if [ ! -f "$f" ]; then echo "build: missing $f"; rc=1; continue; fi if ! json_parse "$f" 2>/dev/null; then echo "build: invalid JSON — $f"; rc=1; fi done # each ADAPTER must have the shape the generic agents rely on node -e ' - for (const f of [".claude/gates.json", ".claude/self/gates.json"]) { + for (const f of [".claude/gates.json", "self/gates.json"]) { const g = require(process.cwd() + "/" + f); if (!g.project || !Array.isArray(g.modules) || typeof g.gates !== "object") { console.error("build: bad adapter shape —", f); process.exit(1); @@ -154,8 +154,8 @@ do_lint() { # .claude/skills/*/*.sh (scaffold.sh, sync.sh) and .claude/skills/*/templates/*.sh # (issue #102's arm-loop.sh template) are included so a syntax regression in the # setup/sync machinery or a scaffolded script template is caught here too, not just - # .claude/scripts/*.sh and .claude/self/*.sh. - for f in .claude/scripts/*.sh .claude/self/*.sh .claude/skills/*/*.sh .claude/skills/*/templates/*.sh; do + # .claude/scripts/*.sh and self/*.sh. + for f in .claude/scripts/*.sh self/*.sh .claude/skills/*/*.sh .claude/skills/*/templates/*.sh; do [ -e "$f" ] || continue bash -n "$f" || { echo "lint: shell syntax error — $f"; rc=1; } done diff --git a/.claude/self/gates.json b/self/gates.json similarity index 84% rename from .claude/self/gates.json rename to self/gates.json index 1e1250d..19227d5 100644 --- a/.claude/self/gates.json +++ b/self/gates.json @@ -1,5 +1,5 @@ { - "_README": "SELF-ADAPTER (issue #11). Lets this template dogfood itself WITHOUT polluting the shipped placeholder .claude/gates.json (which stays pristine for downstream adopters). The loop uses THIS file only when pointed at it via GATES_FILE=.claude/self/gates.json (see .claude/self/README.md). Gate commands are node/bash-only so they run with no extra tooling installed.", + "_README": "SELF-ADAPTER (issue #11). Lets this template dogfood itself WITHOUT polluting the shipped placeholder .claude/gates.json (which stays pristine for downstream adopters). The loop uses THIS file only when pointed at it via GATES_FILE=self/gates.json (see self/README.md). This file (and the rest of self/) lives at the top-level repo root, OUTSIDE .claude/ (issue #138) — the plugin marketplace source is ./.claude, so anything outside it is structurally never copied into a consumer's plugin cache. Gate commands are node/bash-only so they run with no extra tooling installed.", "project": { "name": "recode", "language": "shell+markdown+js", @@ -14,7 +14,7 @@ { "name": "harness", "path": ".claude", - "description": "Orchestrator machinery: agents, commands, scripts, workflows, self-adapter." + "description": "Orchestrator machinery: agents, commands, scripts, workflows. The self-adapter itself (this file and its siblings) lives at the top-level self/ directory, outside .claude/ (issue #138)." }, { "name": "examples", @@ -28,13 +28,13 @@ } ], "gates": { - "_note": "Static checks in .claude/self/checks.sh; end-to-end fan-out smoke in .claude/self/smoke-fanout.sh (issue #64) — node + bash + git only, so they pass in a bare environment. test_affected stays static-only: it's the Stop-hook fast path and shouldn't pay the smoke's git churn on every stop.", + "_note": "Static checks in self/checks.sh; end-to-end fan-out smoke in self/smoke-fanout.sh (issue #64) — node + bash + git only, so they pass in a bare environment. test_affected stays static-only: it's the Stop-hook fast path and shouldn't pay the smoke's git churn on every stop.", "install": "", - "build": "bash .claude/self/checks.sh build", - "lint": "bash .claude/self/checks.sh lint", + "build": "bash self/checks.sh build", + "lint": "bash self/checks.sh lint", "typecheck": "", - "test": "bash .claude/self/checks.sh test && bash .claude/self/smoke-fanout.sh", - "test_affected": "bash .claude/self/checks.sh test", + "test": "bash self/checks.sh test && bash self/smoke-fanout.sh", + "test_affected": "bash self/checks.sh test", "coverage": "", "coverage_threshold": 0, "e2e": "", diff --git a/.claude/self/pr-loop-self.md b/self/pr-loop-self.md similarity index 64% rename from .claude/self/pr-loop-self.md rename to self/pr-loop-self.md index 3c32242..742d9a0 100644 --- a/.claude/self/pr-loop-self.md +++ b/self/pr-loop-self.md @@ -5,7 +5,7 @@ description: Arm (or re-arm) the self-hosted PR-loop cron and run one tick now **LEGACY path (issue #102).** This is the session-scoped cron. The RECOMMENDED replacement is the cron-less daemon: `systemd --user` supervises `.claude/scripts/loop-daemon.sh` forever, independent of any Claude Code session, and spawns a driver only on an actionable verdict (never on `action=none`). Install it with -`bash .claude/scripts/arm-loop.sh --gates-file .claude/self/gates.json` (run in a real terminal outside Claude +`bash .claude/scripts/arm-loop.sh --gates-file self/gates.json` (run in a real terminal outside Claude Code — see `docs/HARDENING.md` → Caveats). **Never run both this cron and the daemon against this repo at once** — `loop-tick.sh`'s spawn lock makes it *safe* (no double-spawn), merely wasteful (two firing sources burning ticks against the same state). Keep reading below only if you're intentionally using the legacy cron. @@ -15,7 +15,7 @@ You are (re)arming this project's self-hosted PR loop — the loop that works TH (cron jobs die when Claude Code exits and may not persist across restarts even when durable), so it is lost at the start of each new session. This command restores the whole loop in one step. Do BOTH parts. -The repo is derived from the git remote (`bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/bot-gh.sh repo view --json nameWithOwner -q .nameWithOwner`); the bot login defaults to `$BOT_LOGIN`. This command is self-hosting only: it always points every gate and every agent at `GATES_FILE=.claude/self/gates.json` (see `.claude/self/README.md`), never the placeholder root `.claude/gates.json`. +The repo is derived from the git remote (`bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/bot-gh.sh repo view --json nameWithOwner -q .nameWithOwner`); the bot login defaults to `$BOT_LOGIN`. This command is self-hosting only: it always points every gate and every agent at `GATES_FILE=self/gates.json` (see `self/README.md`), never the placeholder root `.claude/gates.json`. ## 1. (Re)arm the cron — idempotent - Call `CronList`. If a job already exists whose prompt mentions "self-hosted PR loop", leave it (do not duplicate) and report its id + schedule. (This job's armed prompt never contains the base loop's exact marker substring "autonomous PR loop" — it says "self-hosted PR loop" instead — so `/pr-loop`'s idempotency check, which matches on "autonomous PR loop", will never match this job. Conversely, the base loop's armed prompt never contains "self-hosted PR loop", so this command's idempotency check will never match the base loop's job. The two loops therefore never cross-match in either direction.) @@ -23,18 +23,18 @@ The repo is derived from the git remote (`bash ${CLAUDE_PLUGIN_ROOT:-.claude}/sc Prompt to use (the tick logic, with adaptive STEP 0): -> Run one tick of the self-hosted PR loop. Export `GATES_FILE=.claude/self/gates.json` for every gate/orchestration step, and instruct every spawned agent (orchestrator, implementers, reviewers) to read `.claude/self/gates.json` — NOT the placeholder root `.claude/gates.json` — as its adapter (module map, gates, review lenses). Reviewer lenses + consensus per `.claude/self/gates.json` (`correctness`, `tests`; consensus `all`). Every `gate.sh` invocation MUST be run as `GATES_FILE=.claude/self/gates.json bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/gate.sh `. ALL `gh` interaction (yours and every agent's) MUST run as the bot via `.claude/scripts/bot-gh.sh` — never bare `gh`; only `git` commits/pushes stay as the owner. +> Run one tick of the self-hosted PR loop. Export `GATES_FILE=self/gates.json` for every gate/orchestration step, and instruct every spawned agent (orchestrator, implementers, reviewers) to read `self/gates.json` — NOT the placeholder root `.claude/gates.json` — as its adapter (module map, gates, review lenses). Reviewer lenses + consensus per `self/gates.json` (`correctness`, `tests`; consensus `all`). Every `gate.sh` invocation MUST be run as `GATES_FILE=self/gates.json bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/gate.sh `. ALL `gh` interaction (yours and every agent's) MUST run as the bot via `.claude/scripts/bot-gh.sh` — never bare `gh`; only `git` commits/pushes stay as the owner. > -> STEP 0 — run the tick. Invoke, as a REAL bash tool call, exactly: `GATES_FILE=.claude/self/gates.json bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/loop-tick.sh`. This one script runs census, then notify-poll.sh, merge-ready.sh, pr-feedback.sh, pr-comment-fix.sh, pr-ci-fix.sh, and pr-rebase.sh, IN ORDER, with their full output, and ends with exactly one machine-readable verdict line as the LAST line of output: `action=none`, `action=advance issue=N`, `action=feedback pr=N`, `action=comment-fix pr=N`, `action=ci-fix pr=N`, or `action=rebase pr=N`. That verdict line is the SOLE source of truth for what to do next: never hand-count open PRs, planned issues, feedback PRs, comment-fix PRs, CI-red PRs, or unmergeable PRs yourself, and never skip this invocation because the tick "looks quiet" — it must run, and its output must be read, on every single tick with no exceptions. (loop-census.sh reads `$GATES_FILE` directly, so it derives module labels from the self set — `module:docs`, `module:harness`, `module:examples`, `module:ci` — not the placeholder root adapter.) +> STEP 0 — run the tick. Invoke, as a REAL bash tool call, exactly: `GATES_FILE=self/gates.json bash ${CLAUDE_PLUGIN_ROOT:-.claude}/scripts/loop-tick.sh`. This one script runs census, then notify-poll.sh, merge-ready.sh, pr-feedback.sh, pr-comment-fix.sh, pr-ci-fix.sh, and pr-rebase.sh, IN ORDER, with their full output, and ends with exactly one machine-readable verdict line as the LAST line of output: `action=none`, `action=advance issue=N`, `action=feedback pr=N`, `action=comment-fix pr=N`, `action=ci-fix pr=N`, or `action=rebase pr=N`. That verdict line is the SOLE source of truth for what to do next: never hand-count open PRs, planned issues, feedback PRs, comment-fix PRs, CI-red PRs, or unmergeable PRs yourself, and never skip this invocation because the tick "looks quiet" — it must run, and its output must be read, on every single tick with no exceptions. (loop-census.sh reads `$GATES_FILE` directly, so it derives module labels from the self set — `module:docs`, `module:harness`, `module:examples`, `module:ci` — not the placeholder root adapter.) > > CADENCE: the script's `=== 1/7 loop-census.sh ===` section includes a line `cadence=FAST|WATCH|IDLE cron=` — this is the desired cadence; consume it as-is, do NOT re-derive it from counts. If this cron job's current schedule differs from that `cron=`, CronDelete this job and CronCreate a durable replacement with this SAME prompt at the desired schedule. > > Then obey the verdict line (the tick already ran poll/merge/feedback-detection/comment-fix-detection/ci-fix-detection/rebase-detection above — do not re-run those scripts). Precedence when more than one is ready: `feedback` > `comment-fix` > `ci-fix` > `rebase` > `advance` (a PR with unaddressed feedback outranks everything; a PR with an unresolved qualifying review-comment thread outranks a CI fix, a rebase, or a fresh advance; a CI fix outranks a rebase or a fresh advance; an unmergeable PR needing a rebase outranks only a fresh advance): -> - `action=feedback pr=N` → address PR N's feedback: run orchestrator → worktree implementer → reviewer lenses (self adapter: `GATES_FILE=.claude/self/gates.json`, lenses `correctness`/`tests`, consensus `all`) on the SAME branch, push to update the PR in place, and post the `` marker comment via bot-gh.sh. Do NOT merge. +> - `action=feedback pr=N` → address PR N's feedback: run orchestrator → worktree implementer → reviewer lenses (self adapter: `GATES_FILE=self/gates.json`, lenses `correctness`/`tests`, consensus `all`) on the SAME branch, push to update the PR in place, and post the `` marker comment via bot-gh.sh. Do NOT merge. > - `action=comment-fix pr=N` → address PR N's unresolved review-comment thread(s): label the PR `claude-comment-fixing` via bot-gh.sh first (in-flight guard), then run orchestrator → worktree implementer on the SAME branch (checkout the PR's existing branch, do NOT create a new one) → reviewer lenses (self adapter, same lenses/consensus as feedback), push to update the PR in place. For each thread you actually addressed, resolve it on GitHub and post a bot comment containing `` for each (the real thread id and attempt number from the `5/7 pr-comment-fix.sh` section above) so pr-comment-fix.sh's cursor recognizes it as addressed. Do NOT merge, and do NOT force-push. > - `action=ci-fix pr=N` → fix PR N's failing CI: label the PR `claude-ci-fixing` via bot-gh.sh first (in-flight guard), then run orchestrator → worktree implementer on the SAME branch (checkout the PR's existing branch, do NOT create a new one) → reviewer lenses (self adapter, same lenses/consensus as feedback) to fix the failure, push to update the PR in place. After pushing, query the PR's current head SHA (`bot-gh.sh pr view N --json headRefOid`) and post a bot comment containing exactly `` (the real SHA substituted in) so pr-ci-fix.sh's cursor recognizes this head as already addressed. Do NOT merge, and do NOT force-push. -> - `action=rebase pr=N` → rebase PR N onto base: label the PR `claude-rebasing` via bot-gh.sh first (in-flight guard), checkout the PR's EXISTING branch/worktree (do NOT create a new one), `git fetch` then `git rebase origin/main`. On a CLEAN rebase: `git push --force-with-lease`, rerun the self adapter's gates (`GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh `), and post a bot comment containing `` (the real base SHA and attempt number from the `7/7 pr-rebase.sh` section above) so pr-rebase.sh's cursor recognizes this attempt. On a CONFLICT: `git rebase --abort` (never leave the worktree mid-rebase), label the PR `needs-human`, and post a comment explaining the conflict (also containing the same marker). NEVER merge, and NEVER force-push anything but this bot-owned branch. -> - `action=advance issue=N` → advance issue N through the orchestrator using `.claude/self/gates.json` as the adapter (scope → worktree implementer → `GATES_FILE=.claude/self/gates.json gate.sh` gates → reviewer lenses `correctness`/`tests` consensus `all` → bot PR). One issue in flight at a time. `backlog` issues are owner-unapproved: never pick them, and if you file an issue yourself, label it `backlog` — NEVER `planned` (that label is the owner's formal approval and is assigned by the owner alone; see docs/USAGE.md → "Autonomous loop & the issue queue"). +> - `action=rebase pr=N` → rebase PR N onto base: label the PR `claude-rebasing` via bot-gh.sh first (in-flight guard), checkout the PR's EXISTING branch/worktree (do NOT create a new one), `git fetch` then `git rebase origin/main`. On a CLEAN rebase: `git push --force-with-lease`, rerun the self adapter's gates (`GATES_FILE=self/gates.json bash .claude/scripts/gate.sh `), and post a bot comment containing `` (the real base SHA and attempt number from the `7/7 pr-rebase.sh` section above) so pr-rebase.sh's cursor recognizes this attempt. On a CONFLICT: `git rebase --abort` (never leave the worktree mid-rebase), label the PR `needs-human`, and post a comment explaining the conflict (also containing the same marker). NEVER merge, and NEVER force-push anything but this bot-owned branch. +> - `action=advance issue=N` → advance issue N through the orchestrator using `self/gates.json` as the adapter (scope → worktree implementer → `GATES_FILE=self/gates.json gate.sh` gates → reviewer lenses `correctness`/`tests` consensus `all` → bot PR). One issue in flight at a time. `backlog` issues are owner-unapproved: never pick them, and if you file an issue yourself, label it `backlog` — NEVER `planned` (that label is the owner's formal approval and is assigned by the owner alone; see docs/USAGE.md → "Autonomous loop & the issue queue"). > - `action=none` → reply exactly one line: "No actionable activity." This is the ONLY path to that phrase — never reply it without loop-tick.sh having actually been invoked (and its output read) earlier in this same turn. > > Token discipline: only read docs/USAGE.md and .claude/agents/* when the verdict actually requires orchestrating agents (advance/feedback/ci-fix/rebase); an `action=none` tick needs only the script's own output. Keep the tick report to a few lines — it is telemetry, not documentation. @@ -42,4 +42,4 @@ Prompt to use (the tick logic, with adaptive STEP 0): ## 2. Run one tick now Execute the tick logic above immediately so the loop doesn't wait for the next cron fire. Report what happened (polled items, merges, feedback addressed, issue advanced — or "No actionable activity"). -Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. Cadence is adaptive and biased toward cheap ticks (each tick is a fresh full-context session): FAST (every minute) only while the loop has actionable work — unaddressed PR feedback, or a `planned` self `module:*` issue ready to advance with no PR in flight; WATCH (every 5 minutes) while PRs wait on human review/CI; IDLE (every 15 minutes) otherwise. New work is picked up within one WATCH/IDLE interval. This file lives under `.claude/self/` (not `.claude/commands/`, see issue #76), so it is not a registered slash command — for a tighter in-session cadence, ask Claude to re-read and re-run this file's instructions on the interval you want instead of `/loop 5m /pr-loop-self`. +Notes: requires the bot machine account set up per docs/USAGE.md (`GH_BOT_TOKEN` in `.env`, bot is a write collaborator) so PRs are bot-authored and the owner can formally Approve them. Cadence is adaptive and biased toward cheap ticks (each tick is a fresh full-context session): FAST (every minute) only while the loop has actionable work — unaddressed PR feedback, or a `planned` self `module:*` issue ready to advance with no PR in flight; WATCH (every 5 minutes) while PRs wait on human review/CI; IDLE (every 15 minutes) otherwise. New work is picked up within one WATCH/IDLE interval. This file lives under `self/` (not `.claude/commands/`, see issue #76), so it is not a registered slash command — for a tighter in-session cadence, ask Claude to re-read and re-run this file's instructions on the interval you want instead of `/loop 5m /pr-loop-self`. diff --git a/.claude/self/smoke-fanout.sh b/self/smoke-fanout.sh similarity index 97% rename from .claude/self/smoke-fanout.sh rename to self/smoke-fanout.sh index 3c4ab95..23d8bf0 100644 --- a/.claude/self/smoke-fanout.sh +++ b/self/smoke-fanout.sh @@ -23,9 +23,9 @@ # commit/merge all work there under the strict sandbox). set -uo pipefail -root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" fixture="$root/examples/fixture-target" -patches="$root/.claude/self/smoke" +patches="$root/self/smoke" fail() { echo "smoke: FAIL — $*" >&2; exit 1; } step() { echo "smoke: $*"; } @@ -79,7 +79,7 @@ step "module boundary respected ($mod); change committed on branch" # --- 4. Gates through the REAL gate.sh, inside the worktree ------------------ # env -u GATES_FILE: when this smoke itself runs under the self adapter (CI sets -# GATES_FILE=.claude/self/gates.json), the inner gate.sh would inherit it, fail +# GATES_FILE=self/gates.json), the inner gate.sh would inherit it, fail # to find that path inside the FIXTURE repo, and skip every gate — passing # vacuously even on broken code. The fixture is a consumer repo: it must read # its own default .claude/gates.json. (Caught by the failure-path check below.) diff --git a/.claude/self/smoke/broken.patch b/self/smoke/broken.patch similarity index 100% rename from .claude/self/smoke/broken.patch rename to self/smoke/broken.patch diff --git a/.claude/self/smoke/implementer.patch b/self/smoke/implementer.patch similarity index 100% rename from .claude/self/smoke/implementer.patch rename to self/smoke/implementer.patch From e8454bebbf6493d64070b84b3f31af321457e920 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:54:59 +0200 Subject: [PATCH 2/3] revert(ci): keep .github/workflows/gates.yml unchanged (workflow-scope push restriction) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/gates.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index dc42b2b..a0ef7ef 100644 --- a/.github/workflows/gates.yml +++ b/.github/workflows/gates.yml @@ -45,8 +45,8 @@ jobs: # This repo self-hosts on the orchestrator template: harness changes under # `.claude/**` and `docs/**` are exercised via the SELF adapter - # (self/gates.json), whose gates are actually implemented (node+bash - # checks in self/checks.sh). The `gates` job above reads the default, + # (.claude/self/gates.json), whose gates are actually implemented (node+bash + # checks in .claude/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,5 +64,5 @@ jobs: # 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: self/gates.json + GATES_FILE: .claude/self/gates.json run: bash .claude/scripts/gate.sh ${{ matrix.gate }} From 0e7ce97800fd92a61c6fdfd4fb4018eb155e7e06 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:10:36 +0200 Subject: [PATCH 3/3] fix(self): add structural packaging-exclusion invariant check (issue #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). --- self/checks.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/self/checks.sh b/self/checks.sh index c23f935..2bb9050 100644 --- a/self/checks.sh +++ b/self/checks.sh @@ -35,9 +35,35 @@ do_build() { ' || rc=1 [ "$rc" -eq 0 ] && echo "build: JSON configs valid + adapters well-shaped" do_hooks_parity || rc=1 + do_packaging_exclusion || rc=1 return "$rc" } +# do_packaging_exclusion (issue #138 review) — self-dev assets were moved from +# .claude/self/ to top-level self/ because the plugin installer does a +# verbatim recursive copy of the marketplace-sourced .claude/ directory with +# no exclusion mechanism (see PR #203's investigation): anything left under +# .claude/self/ ships to every consumer's plugin cache. This structurally +# asserts the invariant holds by checking FILE EXISTENCE only — fails the +# build if .claude/self/ (or anything under it) exists again, e.g. from a +# careless future edit that recreates the old path. +# +# Deliberately NOT a content/string check ("no tracked file mentions +# .claude/self/"): .github/workflows/gates.yml still legitimately carries a +# dangling .claude/self/gates.json reference (this session's git credential +# lacks GitHub `workflow` scope, so that file couldn't be updated in this PR +# — see the PR body's required owner follow-up). A string-based assertion +# would wrongly fail the build on that known, intentional exception. +do_packaging_exclusion() { + if [ -e ".claude/self" ]; then + echo "build: packaging-exclusion invariant violated — .claude/self/ exists (self-dev assets must live only under self/, never re-enter the shipped .claude/ tree)" + find .claude/self -type f 2>/dev/null | sed 's/^/build: /' + return 1 + fi + echo "build: packaging-exclusion OK — .claude/self/ does not exist" + return 0 +} + # do_hooks_parity (issue #140) — the repo-local .claude/settings.json (self- # hosted sessions, $CLAUDE_PROJECT_DIR paths) and the shipped # .claude/hooks/hooks.json (consumers, ${CLAUDE_PLUGIN_ROOT} paths) hand-